<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>星光</title><description>深度学习知识分享网站</description><link>https://xingguang641.com/</link><language>zh_CN</language><item><title>【ACM 算法题单】分类讨论相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/problem-solving/case-analysis/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/problem-solving/case-analysis/</guid><description>记录一些 ACM 常见题型</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;分类讨论题目合集&lt;/h1&gt;
&lt;h2&gt;连贯字符串问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-flips-to-make-binary-string-coherent/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法随笔】分块数据结构的应用</title><link>https://xingguang641.com/posts/acm/acm-note/block-structure/block-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/block-structure/block-structure/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇博客写作灵感来源于灵神的分块思想讲解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=116628561858500&amp;amp;bvid=BV16FG76JEQo&amp;amp;cid=38576590372&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>【博客指南】微博图片报错解决方案</title><link>https://xingguang641.com/posts/blog/blog-guide/healing-layer/healing-layer/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/healing-layer/healing-layer/</guid><description>解决老博客的新浪图片显示错误问题</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;图片报错问题来源&lt;/h2&gt;
&lt;p&gt;经常浏览技术博客的读者可能会发现，某些博客中使用的图片会无法正常显示，通常是因为这些图片这些图片都是托管在新浪微博上，而新浪微博早在某次更新后就启用了 &lt;strong&gt;防盗链（Referer Hotlinking Protection）&lt;/strong&gt; 机制。简单来说，当浏览器请求图片时，新浪服务器会校验 HTTP 请求头中的 &lt;code&gt;Referer&lt;/code&gt; 字段（即请求来源）。如果 &lt;code&gt;Referer&lt;/code&gt; 显示请求来源于你的个人博客域名，而非新浪微博的官方域名，服务器就会拦截请求并返回 403 错误。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CBlog%5Cblog-guide%5Chealing-layer%5C%E5%9B%BE%E7%89%87%E6%8A%A5%E9%94%991.png&quot; alt=&quot;图片报错示例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要解决这一问题，核心在于 &lt;strong&gt;重构请求的上下文信息&lt;/strong&gt; 。既然问题的根源在于新浪服务器对请求来源的审计，我们完全可以通过浏览器扩展来实时干预：利用插件动态篡改发往新浪域名的 HTTP 请求头，将 &lt;code&gt;Referer&lt;/code&gt; 字段重置为合法的官方域名。通过这种身份伪装，我们即可实时绕过服务端的校验机制，让那些原本受限的资源在我们的浏览器中无感恢复。&lt;/p&gt;
&lt;h2&gt;图片报错解决方案&lt;/h2&gt;
&lt;h3&gt;1. 安装扩展插件&lt;/h3&gt;
&lt;p&gt;在 Chrome、Edge 或 Firefox 的扩展商店中搜索并安装 &lt;strong&gt;Header Editor&lt;/strong&gt; 插件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CBlog%5Cblog-guide%5Chealing-layer%5CHeader-Editor1.jpg&quot; alt=&quot;插件商城图像&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 添加修改规则&lt;/h3&gt;
&lt;p&gt;安装完成后，点击插件图标进入管理界面，点击右下角或右上角的 &lt;strong&gt;“添加”&lt;/strong&gt; 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CBlog%5Cblog-guide%5Chealing-layer%5CHeader-Editor2.jpg&quot; alt=&quot;插件商城图像&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 配置规则详情&lt;/h3&gt;
&lt;p&gt;这是最关键的一步，请按照以下逻辑进行配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规则类型&lt;/strong&gt;：选择 &lt;strong&gt;修改请求头&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匹配规则&lt;/strong&gt;：选择 &lt;strong&gt;域名&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头名称&lt;/strong&gt;：输入 &lt;code&gt;Referer&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头内容&lt;/strong&gt;：输入 &lt;code&gt;https://weibo.com&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CBlog%5Cblog-guide%5Chealing-layer%5CHeader-Editor3.jpg&quot; alt=&quot;插件商城图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CBlog%5Cblog-guide%5Chealing-layer%5CHeader-Editor4.jpg&quot; alt=&quot;插件商城图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于匹配规则的说明&lt;/strong&gt;：在 &lt;strong&gt;“匹配类型”&lt;/strong&gt; 一栏中，你需要填入图片链接的域名。你可以按 &lt;code&gt;F12&lt;/code&gt; 打开开发者工具，选中无法显示的图片，就能查看其 URL。如果图片地址是 &lt;code&gt;https://ws1.sinaimg.cn/large/...&lt;/code&gt; ，那么你应该填入 &lt;code&gt;ws1.sinaimg.cn&lt;/code&gt; 或 &lt;code&gt;sinaimg.cn&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;4. 保存运行测试&lt;/h3&gt;
&lt;p&gt;点击保存，确保插件处于 &lt;strong&gt;“启用”&lt;/strong&gt; 状态。刷新你的博客页面，原本裂开的图片应该就能正常加载了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;进阶提示&lt;/strong&gt;：如果你的博客引用了多个不同服务器的新浪图片（如 &lt;code&gt;ws1.sinaimg.cn&lt;/code&gt; ，&lt;code&gt;wx3.sinaimg.cn&lt;/code&gt; 等），建议将匹配规则改为正则表达式，并输入 &lt;code&gt;.*sinaimg.cn.*&lt;/code&gt; ，这样可以一次性匹配所有新浪图床的子域名。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>【ACM 算法随笔】Trick算法汇总</title><link>https://xingguang641.com/posts/acm/acm-note/trick-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/trick-algorithms/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;参考文献列表&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/problem-solving/case-analysis/&quot;&gt;【ACM 算法题单】分类讨论相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/problem-solving/meet-in-middle/&quot;&gt;【ACM 算法题单】折半搜索相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/problem-solving/incremental-algorithm/&quot;&gt;【ACM 算法题单】增量算法相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/problem-solving/equivalent-substitution/&quot;&gt;【ACM 算法题单】等价变换相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】区间数据结构优化问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-optimization/interval-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-optimization/interval-structure/</guid><description>记录一些 ACM 常见题型</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】单调数据结构优化问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-optimization/monotonic-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-optimization/monotonic-structure/</guid><description>记录一些 ACM 常见题型</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;递推型问题优化&lt;/h1&gt;
&lt;h2&gt;道路游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1070&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法题单】组合数学相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-branch/combinatorics/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-branch/combinatorics/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】抽象代数相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-branch/abstract-algebra/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-branch/abstract-algebra/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;置换环的基本性质&lt;/h1&gt;
&lt;h2&gt;交换字符串元素&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/smallest-string-with-swaps/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;执行交换操作后的最小汉明距离&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimize-hamming-distance-after-swap-operations/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;佳佳的篝火晚会&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1053&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;简单的循环系统&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P7981&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/math/permutation/&quot;&gt;【OI WiKi】置换和排列相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.miyoushe.com/wd/article/40834894&quot;&gt;【米游社专栏】置换环の使用说明书&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】计算几何相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-branch/computational-geometry/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-branch/computational-geometry/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】线性代数相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-branch/linear-algebra/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-branch/linear-algebra/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】字符串嵌套相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/string-problems/string-nest/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/string-problems/string-nest/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇博客写作灵感来源于左神的递归套路理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=317459231&amp;amp;bvid=BV1JP411p7KG&amp;amp;cid=1239070727&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h1&gt;字符串嵌套相关题&lt;/h1&gt;
&lt;h2&gt;等价表达式问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1054&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法题单】同余原理相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/congruence/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/congruence/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;四则运算同余原理&lt;/h1&gt;
&lt;h3&gt;减法的同余公式&lt;/h3&gt;
&lt;h3&gt;除法逆元及其性质（包括相关算法）&lt;/h3&gt;
&lt;hr /&gt;
&lt;h1&gt;幂次运算同余原理&lt;/h1&gt;
&lt;p&gt;乘法快速幂也能算幂次数的同余，但是不确定是否算在这个板块&lt;/p&gt;
&lt;h3&gt;费马小定理&lt;/h3&gt;
&lt;h3&gt;欧拉定理&lt;/h3&gt;
&lt;h3&gt;扩展欧拉定理&lt;/h3&gt;
&lt;h2&gt;尾数的循环问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1050&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;组合运算同余原理&lt;/h1&gt;
&lt;h3&gt;卢卡斯定理&lt;/h3&gt;
&lt;h3&gt;扩展卢卡斯定理&lt;/h3&gt;
</content:encoded></item><item><title>【ACM 算法随笔】Math算法汇总</title><link>https://xingguang641.com/posts/acm/acm-note/math-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/math-algorithms/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;常见的数学算符&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/mod-problem/&quot;&gt;【ACM 算法题单】MOD相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/abs-problem/&quot;&gt;【ACM 算法题单】ABS相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/gcd-problem/&quot;&gt;【ACM 算法题单】GCD相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/bit-problem/&quot;&gt;【ACM 算法题单】BIT相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/mex-problem/&quot;&gt;【ACM 算法题单】MEX相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;常见的数学分支&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-branch/computational-geometry/&quot;&gt;【ACM 算法题单】计算几何相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-branch/combinatorics/&quot;&gt;【ACM 算法题单】组合数学相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-branch/linear-algebra/&quot;&gt;【ACM 算法题单】线性代数相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-branch/abstract-algebra/&quot;&gt;【ACM 算法题单】抽象代数相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】区间数据结构的应用</title><link>https://xingguang641.com/posts/acm/acm-note/interval-structure/interval-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/interval-structure/interval-structure/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;区间数据结构介绍&lt;/h1&gt;
&lt;p&gt;线段树、树状数组、ST表、主席树&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;令人感伤的红雨&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8010&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;简单的线段树问题（可以用并查集解决，要学会并查集解法）&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法随笔】有序数据结构的应用</title><link>https://xingguang641.com/posts/acm/acm-note/ordered-structure/ordered-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/ordered-structure/ordered-structure/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;有序数据结构介绍&lt;/h1&gt;
&lt;p&gt;AVL树、跳表、替罪羊树、笛卡尔树等等（看左神视频）&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法题单】字符串哈希相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/string-problems/string-hash/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/string-problems/string-hash/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;字符串哈希相关题&lt;/h1&gt;
&lt;p&gt;可以替代KMP的子串比对，代替马拉车记录回文半径，但是要更慢&lt;/p&gt;
&lt;p&gt;本质是将子串比对这个操作优化到 O(1)，这个是优势区间&lt;/p&gt;
&lt;h2&gt;数字频率相同的子字符串的数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.doocs.org/lc/2168/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;字符串去重（相当于暴力做法）&lt;/p&gt;
&lt;h2&gt;最短唯一子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/smallest-unique-subarray/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;二分答案（字符串越短越可能重复）+字符串去重&lt;/p&gt;
&lt;h2&gt;重复叠加字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/repeated-string-match/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;需要找规律，建议记住&lt;/p&gt;
&lt;h2&gt;串联所有的单词&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/substring-with-concatenation-of-all-words/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;简单的字符串哈希+滑动窗口&lt;/p&gt;
&lt;h2&gt;失配字符串问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/substring-with-concatenation-of-all-words/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;字符串哈希+二分找不同点&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://oi-wiki.org/string/hash/&quot;&gt;【OI WiKi】字符串哈希相关知识&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】字符串索引相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/string-problems/string-index/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/string-problems/string-index/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;字符串索引相关题&lt;/h1&gt;
</content:encoded></item><item><title>【ACM 算法题单】差分约束相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/difference-constraints/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/difference-constraints/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】同余最短路相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-modular/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-modular/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】最短路树相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-tree/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-tree/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/86gevhsv&quot;&gt;【Luogu 博客】最短路径树&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】Graph算法汇总</title><link>https://xingguang641.com/posts/acm/acm-note/graph-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/graph-algorithms/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/topological-sort/topological-sort/&quot;&gt;【ACM 算法随笔】拓扑排序算法相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path/&quot;&gt;【ACM 算法题单】最短路算法相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/tree-algorithms/tree-algorithms/&quot;&gt;【ACM 算法题单】树上算法相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】String算法汇总</title><link>https://xingguang641.com/posts/acm/acm-note/string-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/string-algorithms/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;字符匹配算法原理&lt;/h1&gt;
&lt;h2&gt;单模式匹配算法&lt;/h2&gt;
&lt;p&gt;BM 算法    Sunday 算法    KMP 算法（需要强调KMP真正的优点：按位独立性）&lt;/p&gt;
&lt;p&gt;先讲解LSP数组的性质&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com/article/qgl51obr&quot;&gt;Border理论&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/ds5cz0sg&quot;&gt;Border理论小记&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;拓展KMP（Z函数）&lt;/p&gt;
&lt;h2&gt;另一棵树的子树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subtree-of-another-tree/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;需要结合DFN序&lt;/p&gt;
&lt;h2&gt;二叉树中的链表&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-in-binary-tree/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;依旧自上而下DFS降维&lt;/p&gt;
&lt;h2&gt;不断删除字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4824&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;类似消消乐的这种题目统一用栈解决&lt;/p&gt;
&lt;h2&gt;找到好的字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-all-good-strings/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;数位DP+KMP算法，需要了解KMP的特殊性&lt;/p&gt;
&lt;h2&gt;将单词恢复初始状态所需的时间&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-time-to-revert-word-to-initial-state-ii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;Z数组的简单运用&lt;/p&gt;
&lt;h2&gt;多模式匹配算法&lt;/h2&gt;
&lt;p&gt;AC 自动机&lt;/p&gt;
&lt;h2&gt;好字符串的构造&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3311&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;和上面那道数位DP+KMP的题目一模一样，只是改成AC自动机读取多个模式串&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;回文判断算法原理&lt;/h1&gt;
&lt;p&gt;Manacher 算法    回文自动机&lt;/p&gt;
&lt;h2&gt;最长的回文子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-palindromic-substring/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;水题&lt;/p&gt;
&lt;h2&gt;回文子串的数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/palindromic-substrings/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;水题&lt;/p&gt;
&lt;h2&gt;不重叠回文子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-non-overlapping-palindrome-substrings/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;贪心题&lt;/p&gt;
&lt;h2&gt;最长双回文子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4555&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;前后缀分解&lt;/p&gt;
&lt;h2&gt;拉拉队排练问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1659&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;中心点对应一个回文串&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/string-problems/string-hash/&quot;&gt;【ACM 算法题单】字符串哈希相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/string-problems/string-index/&quot;&gt;【ACM 算法题单】字符串索引相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/string-problems/string-nest/&quot;&gt;【ACM 算法题单】字符串嵌套相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】等价变换相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/problem-solving/equivalent-substitution/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/problem-solving/equivalent-substitution/</guid><description>记录一些 ACM 常见题型</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;等价变换题目合集&lt;/h1&gt;
&lt;h2&gt;观光团买票难题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class091/Code03_GroupBuyTickets1.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;某单位共有 $n$ 个人，景区里一共有 $m$ 个项目。每个项目 $i$ 有两个正整数参数：折扣系数 $K_i$ 和票价 $B_i$ 。&lt;/p&gt;
&lt;p&gt;购票规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有 $x$ 个人买票游玩项目 $i$ ，则单张门票的价格为 $\max { B_i - K_i \times x, 0 }$ 。&lt;/li&gt;
&lt;li&gt;$x$ 个人游玩该项目的总花费为 $x \times \max { B_i - K_i \times x, 0 }$ 。由于总花费不会为负数，实际计算公式为 $\max { x \times (B_i - K_i \times x), 0 }$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;单位里的每个人最多可以选择 $1$ 个项目游玩，也可以不选任何项目。所有员工将在明晚提交他们的选择，然后由你统一购票。你需要准备足够多的钱，以应对所有可能的员工选择情况。请返回这个 “最保险” 的钱数（即在所有可能的分配方案中，单位总花费的最大值）。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq m, n, K_i, B_i \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $m$ 和 $n$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $m$ 行，每行包含两个整数，分别表示第 $i$ 个项目的 $K_i$ 和 $B_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$m \quad n$&lt;/p&gt;
&lt;p&gt;$K_1 \quad B_1$&lt;/p&gt;
&lt;p&gt;$K_2 \quad B_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$K_m \quad B_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最保险的准备金额。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;灌溉花园所需的最少水龙头数目&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-taps-to-open-to-water-a-garden/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在 $x$ 轴上有一个长度为 $n$ 的花园，范围从 $0$ 到 $n$ 。&lt;/p&gt;
&lt;p&gt;花园里安装了 $n + 1$ 个水龙头，分别位于 $[0, 1, \dots, n]$ 的位置。给你一个整数 $n$ 和一个长度为 $n + 1$ 的整数数组 &lt;code&gt;ranges&lt;/code&gt; ，其中 &lt;code&gt;ranges[i]&lt;/code&gt; 表示第 $i$ 个水龙头（位于 $i$ 处）的灌溉范围为 $[i - ranges[i], i + ranges[i]]$ 。&lt;/p&gt;
&lt;p&gt;请你求出能够灌溉整个花园 $[0, n]$ 所需的最少水龙头数目。如果花园无法被水龙头全灌溉，请返回 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^4$&lt;/li&gt;
&lt;li&gt;$ranges.length == n + 1$&lt;/li&gt;
&lt;li&gt;$0 \leq ranges[i] \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n + 1$ 个整数，表示数组 $ranges$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$ranges_0 \quad ranges_1 \quad \ldots \quad ranges_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能够灌溉整个花园的最少水龙头数目。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
3 4 1 1 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
0 0 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法题单】折半搜索相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/problem-solving/meet-in-middle/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/problem-solving/meet-in-middle/</guid><description>记录一些 ACM 常见题型</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;折半搜索题目合集&lt;/h1&gt;
&lt;p&gt;折半搜索（也常被称为双向搜索）是我们处理算法问题时 &lt;strong&gt;缓解状态爆炸&lt;/strong&gt; 的经典技巧。其核心思想在于将原本规模为 $n$ 的搜索问题拆分为两个规模约为 $n/2$ 的子问题，通过分别独立搜索再进行结果合并，将指数级增长的复杂度有效降低。这种转化并未从根本上消除问题的指数级特征，而是通过 &lt;strong&gt;路径重构&lt;/strong&gt; ，将原本深层搜索带来的计算压力 &lt;strong&gt;转移到了结果的整合阶段&lt;/strong&gt; 。这种通过拆分搜索任务来平衡计算开销的策略，使得原本因搜索树规模呈指数级扩张而导致时空开销过大的一类问题，在严苛的时限内得以解决。&lt;/p&gt;
&lt;p&gt;搜索过程的简化必然伴随着合并逻辑的复杂化，因此掌握折半搜索的关键在于 &lt;strong&gt;对中间结果的整合&lt;/strong&gt; 。在具体解题中，整合步骤通常需要调用 &lt;strong&gt;排序、二分查找、哈希表或双指针&lt;/strong&gt; 等工具，这意味着我们不仅要能够编写基础的搜索函数，更需要根据问题的 &lt;strong&gt;数值特征与单调性&lt;/strong&gt; ，选择最合适的整合策略。通过系统归纳典型题目的匹配方式，我们能够更直观地理解不同工具的适用场景，从而在面对复杂搜索空间时，构建出 &lt;strong&gt;最高效的整合方案&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;不相邻整数序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc427/tasks/abc427_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的整数序列 $A = (A_1, A_2, \ldots, A_N)$ ，序列 $A$ 的子序列一共有 $2^N$ 种。对于一个子序列 $(A_{i_1}, A_{i_2}, \ldots, A_{i_k})$ ，满足 $1 \leq i_1 &amp;lt; i_2 &amp;lt; \ldots &amp;lt; i_k \leq N$ 。请你统计满足以下两个条件的子序列个数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有被选出的元素在原序列中 &lt;strong&gt;不相邻&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;子序列的元素和是 $M$ 的倍数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;即使两个子序列的值相同，只要取自不同的位置，也视为不同的子序列。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 60$&lt;/li&gt;
&lt;li&gt;$1 \leq M \leq 10^9$&lt;/li&gt;
&lt;li&gt;$0 \leq A_i &amp;lt; M$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示序列 $A$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出满足条件的子序列数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 6
3 1 4 1 5 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15 10
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;798
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;从题目表面上看，这是一个非常典型的 &lt;strong&gt;动态规划&lt;/strong&gt; 问题。在常规的计数场景下，我们可以定义 $dp[i][j][0/1]$ 表示前 $i$ 个数中选出的子序列和取模 $M$ 等于 $j$ ，且当前第 $i$ 位选或不选的方案数。然而，本题的约束条件中 $M$ 高达 $10^9$ ，这导致状态空间中取模维度直接爆炸，常规 DP 无法在有限的时空内维护如此庞大的状态转移表。&lt;/p&gt;
&lt;p&gt;由于 $N$ 的范围在 $60$ 以内，与常见的折半搜索适用区间相吻合，因此我们可以考虑通过这种技巧来解决。我们将序列 $A$ 按下标划分为左右两部分：左半部分为 $A_1 \sim A_k$ ，右半部分为 $A_{k+1} \sim A_N$ ，其中 $k=\lfloor N/2 \rfloor$ 。对每一部分分别进行深度优先搜索，枚举所有满足不相邻约束的子序列状态。&lt;/p&gt;
&lt;p&gt;在枚举左半部分时，对每一个合法子序列记录其元素和对 $M$ 取模后的值 $s$ ，以及一个关键的布尔标记 $t$ ，用来表示该子序列是否选取了左半部分的末尾元素 $A_k$ 。我们将枚举结果按照 $t$ 的取值分为 $L_0$（未选 $A_k$ ）和 $L_1$（已选 $A_k$ ）两组。同理，对右半部分进行枚举，记录模值 $s&apos;$ 以及表示是否选取了右半部分首元素 $A_{k+1}$ 的标记 $u$ ，并将其分为 $R_0$ 和 $R_1$ 两组。&lt;/p&gt;
&lt;p&gt;在最后的合并阶段，我们需要处理跨越分界线的相邻约束。根据不相邻的规则，如果左半部分选了 $A_k$（即来自 $L_1$ ），则右半部分绝对不能选 $A_{k+1}$（即只能匹配 $R_0$ ）；如果左半部分没选 $A_k$（即来自 $L_0$ ），则右半部分可选也可不选（即匹配 $R_0 \cup R_1$ ）。我们需要统计满足以下等式的组合数量：&lt;/p&gt;
&lt;p&gt;$$
(s + s&apos;) \equiv 0 \pmod M
$$&lt;/p&gt;
&lt;p&gt;具体实现时，由于单侧枚举出的状态数在 $2^{30}$ 以内（实际受不相邻约束限制，状态数远小于此），我们可以通过对右半部分的模值进行排序，然后利用 &lt;strong&gt;二分查找&lt;/strong&gt; 快速定位补数 $(M-s) \bmod M$ 的出现次数。通过这种方式，原本无法处理的 $10^9$ 量级模值维度被转化为对两个 $10^6$ 级别序列的扫描与合并，从而在时限内求得最终答案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 100;
int n, mid; ll modM;
ll arr[MAXN];

// cntLeft[t][mod]：左半部分，t = 是否选了arr[mid]
unordered_map&amp;lt;ll, ll&amp;gt; cntLeft[2];
void dfsLeft(int pos, bool lastTaken, bool takeLast, ll sumMod) {
    if (pos &amp;gt; mid) {
        cntLeft[takeLast][sumMod]++;
        return;
    }

    // 不选当前元素
    dfsLeft(pos + 1, false, takeLast, sumMod);
    // 选当前元素（需满足不相邻）
    if (!lastTaken) {
        bool newTakeLast = takeLast;
        if (pos == mid) newTakeLast = true;
        dfsLeft(pos + 1, true, newTakeLast,
               (sumMod + arr[pos]) % modM);
    }
}

// cntRight[u][mod]：右半部分，u = 是否选了arr[mid+1]
unordered_map&amp;lt;ll, ll&amp;gt; cntRight[2];
void dfsRight(int pos, bool lastTaken, bool takeFirst, ll sumMod) {
    if (pos &amp;gt; n) {
        cntRight[takeFirst][sumMod]++;
        return;
    }

    // 不选当前元素
    dfsRight(pos + 1, false, takeFirst, sumMod);
    // 选当前元素（需满足不相邻）
    if (!lastTaken) {
        bool newTakeFirst = takeFirst;
        if (pos == mid + 1) newTakeFirst = true;
        dfsRight(pos + 1, true, newTakeFirst,
                (sumMod + arr[pos]) % modM);
    }
}

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; modM;
    for (int i = 1; i &amp;lt;= n; i++) {
        cin &amp;gt;&amp;gt; arr[i];
    }

    mid = n / 2;
    dfsLeft(1, false, false, 0);
    dfsRight(mid + 1, false, false, 0);

    ll answer = 0;
    for (int takeLast = 0; takeLast &amp;lt;= 1; takeLast++) {
        for (auto &amp;amp;[leftMod, leftCount] : cntLeft[takeLast]) {
            ll need = (modM - leftMod) % modM;
            if (takeLast) {
                // 左边选了 arr[mid]，右边不能选 arr[mid+1]
                if (cntRight[0].count(need)) {
                    answer += leftCount * cntRight[0][need];
                }
            } else {
                // 左边没选 arr[mid]，右边随意
                if (cntRight[0].count(need)) {
                    answer += leftCount * cntRight[0][need];
                }
                if (cntRight[1].count(need)) {
                    answer += leftCount * cntRight[1][need];
                }
            }
        }
    }

    cout &amp;lt;&amp;lt; answer &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通往整数的路径&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc402/tasks/abc402_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个 $N \times N$ 的格子。格子中每个位置 $(i, j)$ 都写有一个数字 $A_{i,j}$（取值范围是 $1$ 到 $9$ ）。目前棋子最开始放在格子 $(1,1)$ 。你需要进行以下操作 &lt;strong&gt;共 $2N-1$ 次&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;把当前棋子所在格子中的数字 &lt;strong&gt;追加到字符串 $S$&lt;/strong&gt; 的末尾&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后把棋子朝 &lt;strong&gt;下方或右方&lt;/strong&gt; 移动一格&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意：在第 $2N-1$ 次操作中不再移动棋子&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行完这些操作后，棋子最终会到达格子 $(N, N)$ ，并且字符串 $S$ 的长度为 $2N-1$ 。把字符串 $S$ 视为一个十进制整数，然后对 $M$ 取模后的余数就是得分。你的任务是输出可以得到的 &lt;strong&gt;最大得分&lt;/strong&gt; 。也就是说，你可以在每次移动时任意选择向右或者向下，但必须最后从 $(1,1)$ 到达 $(N,N)$ ，并要 &lt;strong&gt;最大化 $S \bmod M$&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 20$&lt;/li&gt;
&lt;li&gt;$2 \leq M \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq A_{ij} \leq 9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ 。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行包含 $N$ 个整数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$A_{11} \quad A{12} \quad \ldots \quad A_{1N}$&lt;/p&gt;
&lt;p&gt;$A_{21} \quad A{22} \quad \ldots \quad A_{2N}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_{N1} \quad A{N2} \quad \ldots \quad A_{NN}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示可以达到的最大得分 （即最大化字符串 $S$ 对 $M$ 取模的结果）。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 7
1 2
3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 100000
1 2 3
3 5 8
7 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;13712
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 402
8 1 3 8 9
8 2 4 1 8
4 1 8 5 9
6 2 1 6 7
6 6 7 7 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;384
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;从 $(1,1)$ 走到 $(N,N)$ 的合法路径共有 $C_{2N-2}^{N-1}$ 条，当 $N = 20$ 时，路径总数约为 $3.5 \times 10^{10}$ 。在如此庞大的搜索空间下，直接枚举所有路径显然会超出时间限制。因此，我们可以采用 &lt;strong&gt;折半搜索&lt;/strong&gt; 的策略，在中间层将路径一分为二。我们考虑所有满足 $i + j = N + 1$ 的格子作为分界点，这样任意一条完整路径都可以被拆分为 &lt;strong&gt;从 $(1,1)$ 到某个中点格子的前半段路径&lt;/strong&gt; 和 &lt;strong&gt;从该中点格子到 $(N,N)$ 的后半段路径&lt;/strong&gt; 两个部分。&lt;/p&gt;
&lt;p&gt;对于前半段路径，我们枚举所有从 $(1,1)$ 走到每个中点 $(i,j)$ 的路径，并计算对应字符串表示的数值对 $M$ 取模的结果。由于拼接采用十进制，我们在转移时维护变量 $h$ ，并对每一个中点格子记录所有可能的前半段模值集合：&lt;/p&gt;
&lt;p&gt;$$
h = (h_{prev} \times 10 + A_{i,j}) \bmod M
$$&lt;/p&gt;
&lt;p&gt;对于后半段路径，我们反向枚举所有从 $(N,N)$ 走到每个中点 $(i, j)$ 的路径，并计算对应字符串表示的数值对 $M$ 取模的结果。由于中点值 $A_{i,j}$ 已经在前半段计入，要注意排除这个中点值。并且我们是逆向求解，因此要多维护一个变量 $k$ 表示当前格子距离终点的步数：&lt;/p&gt;
&lt;p&gt;$$
t = (t_{prev} + A_{i,j} \times 10^k) \bmod M
$$&lt;/p&gt;
&lt;p&gt;在合并阶段，对于每一个中点格子，我们已经拥有该处所有可能的前半段集合 ${h}$ 和后半段集合 ${t}$ 。如果直接枚举所有 $(h, t)$ 的组合，时间复杂度将达到平方量级，在大规模数据下无法通过，因此我们需要找到更高效的合并方法。首先我们把问题形式化，将前半段数值向左平移 $N-1$ 位，得到合并公式：&lt;/p&gt;
&lt;p&gt;$$
S \bmod M = (h \times 10^{N-1} + t) \bmod M
$$&lt;/p&gt;
&lt;p&gt;为了最大化这个结果，我们可以令 $x = (h \times 10^{N-1}) \bmod M$ ，那么这个问题就变成经典的&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/mod-problem/#mod%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E9%97%AE%E9%A2%98&quot;&gt;两数之和取模问题&lt;/a&gt;。由于在合并前 $x$ 和 $t$ 均已对 $M$ 取模，它们的取值范围均在 $[0, M)$ 之内。这意味着两数之和对 $M$ 取模只有两种可能的结果：&lt;/p&gt;
&lt;p&gt;$$
(x + t) \bmod M =
\begin{cases}
x + t, &amp;amp; x + t &amp;lt; M \
x + t - M, &amp;amp; M \le x + t &amp;lt; 2M
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;基于这一观察，当我们要最大化结果时，优先目标是让 $x+t$ 尽量接近但不超过 $M$ ；如果无法做到，则退而求其次选择 $x+t$ 尽量大的情况。因此，在固定 $x$ 的前提下，我们只需在已排序的 ${t}$ 集合中，通过二分寻找满足 $t &amp;lt; M - x$ 的最大值，或者直接取 ${t}$ 中的最大值进行尝试，这两种情况必然覆盖了所有可能的最优解。通过这种方式，我们可以将合并阶段的复杂度从平方量级降低为 $O(n \log n)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 25;
int n; ll modM;
ll grid[MAXN][MAXN];
// pow10[i] = 10^i % modM
ll pow10[MAXN * 2];

// leftVals[i][j]：从 (1,1) 到 (i,j) 的所有前缀模值
vector&amp;lt;ll&amp;gt; leftVals[MAXN][MAXN];
void dfsLeft(int x, int y, ll curVal) {
    curVal = (curVal * 10 + grid[x][y]) % modM;
    if (x + y == n + 1) {
        leftVals[x][y].push_back(curVal);
        return;
    }

    if (x + 1 &amp;lt;= n) dfsLeft(x + 1, y, curVal);
    if (y + 1 &amp;lt;= n) dfsLeft(x, y + 1, curVal);
}

// rightVals[i][j]：从 (i,j) 到 (n,n) 的所有后缀模值（不包含 (i,j)）
vector&amp;lt;ll&amp;gt; rightVals[MAXN][MAXN];
void dfsRight(int x, int y, ll curVal, int len) {
    // 到中点就停，不再把 grid[x][y] 加进去
    if (x + y == n + 1) {
        rightVals[x][y].push_back(curVal);
        return;
    }
    curVal = (grid[x][y] * pow10[len] + curVal) % modM;

    if (x - 1 &amp;gt;= 1) dfsRight(x - 1, y, curVal, len + 1);
    if (y - 1 &amp;gt;= 1) dfsRight(x, y - 1, curVal, len + 1);
}

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; modM;
    for (int i = 1; i &amp;lt;= n; i++) {
        for (int j = 1; j &amp;lt;= n; j++) {
            cin &amp;gt;&amp;gt; grid[i][j];
        }
    }
    // 预处理 10 的幂
    pow10[0] = 1 % modM;
    for (int i = 1; i &amp;lt;= 2 * n; i++) {
        pow10[i] = (pow10[i - 1] * 10) % modM;
    }

    dfsLeft(1, 1, 0);
    dfsRight(n, n, 0, 0);

    ll answer = 0;
    for (int i = 1; i &amp;lt;= n; i++) {
        int j = n + 1 - i;
        if (j &amp;lt; 1 || j &amp;gt; n) continue;

        int rightLen = (n - i) + (n - j);
        auto &amp;amp;L = leftVals[i][j];
        auto &amp;amp;R = rightVals[i][j];
        if (L.empty() || R.empty()) continue;

        sort(R.begin(), R.end());
        for (ll leftVal : L) {
            ll x = (leftVal * pow10[rightLen]) % modM;
            ll need = (modM - x) % modM;

            // 尝试 a + b &amp;lt; M 的最大情况
            auto it = lower_bound(R.begin(), R.end(), need);
            if (it != R.begin()) {
                --it;
                answer = max(answer, (x + *it) % modM);
            }

            // 尝试 a + b &amp;gt;= M 的情况
            answer = max(answer, (x + R.back()) % modM);
        }
    }

    cout &amp;lt;&amp;lt; answer &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2501_90415399/article/details/159130103&quot;&gt;【CSDN 博客】C++ 折半搜索&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/zhouyiran2011/p/18874958&quot;&gt;【zhouyiran2011】折半搜索&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】增量算法相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/problem-solving/incremental-algorithm/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/problem-solving/incremental-algorithm/</guid><description>记录一些 ACM 常见题型</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;增量算法题目合集&lt;/h1&gt;
&lt;p&gt;增量法是一类基于 &lt;strong&gt;单调性&lt;/strong&gt; 或 &lt;strong&gt;局部可更新性&lt;/strong&gt; 的处理技巧。其核心思想在于：通过人为构造一种状态演进的顺序，使得在处理每一个新元素时，我们仅需对受影响的局部变化进行维护，而非推倒重来进行全局计算。正因为这种方法高度依赖于我们对处理顺序的 &lt;strong&gt;主动设计&lt;/strong&gt; ，它更像是一种底层思考方式而非固定的算法模板。无论是通过排序建立单调性，还是通过扫描线动态演进，本质上都是在利用已知状态与新状态之间的强关联来降低计算成本。&lt;/p&gt;
&lt;p&gt;由于不存在一套能机械套用的统一流程，增量法的难点并不在于编码细节，而在于我们能否从错综复杂的约束中，通过 &lt;strong&gt;构造合适的处理路径&lt;/strong&gt; ，敏锐地识别出具备增量维护特征的递推条件。通过对题目结构特征的深入剖析，我们可以准确识别出哪些 &lt;strong&gt;难以处理的复杂约束&lt;/strong&gt; 适合通过增量方式进行化解，并以此为突破口建立状态间的递推联系。这种思维习惯能够帮助我们敏锐地察觉到题目约束中的突破点，从而在面对复杂的动态问题时，能够通过主动构造处理路径，将原本棘手的全局性限制转化为可控的局部更新。&lt;/p&gt;
&lt;h2&gt;限距的下降跳跃&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc408/tasks/abc408_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 &lt;code&gt;N&lt;/code&gt; 个脚手架排成一排，编号为 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;N&lt;/code&gt; 。第 &lt;code&gt;i&lt;/code&gt; 个脚手架的高度为 &lt;code&gt;H_i&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;高桥将进行如下游戏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开始时，他可以任选一个脚手架 &lt;code&gt;i&lt;/code&gt; 站上去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前站在脚手架 &lt;code&gt;i&lt;/code&gt;，他可以移动到脚手架 &lt;code&gt;j&lt;/code&gt;，当且仅当满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 ≤ |i - j| ≤ R&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;H_j ≤ H_i - D&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标脚手架与当前位置的距离 &lt;strong&gt;不超过 &lt;code&gt;R&lt;/code&gt; 且不为 0&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;目标脚手架的高度 &lt;strong&gt;至少比当前脚手架低 &lt;code&gt;D&lt;/code&gt;&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;高桥会不断移动，直到 &lt;strong&gt;无法再进行合法移动为止&lt;/strong&gt; 。求他最多可以移动多少次。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 5 × 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq D, R \leq N$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$D$ 、$R$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数 $H_i$ ，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad D \quad R$&lt;/p&gt;
&lt;p&gt;$H_1 \quad H_2 \quad \ldots \quad H_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多可以进行的移动次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 2 1
5 3 1 4 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;13 3 2
13 7 10 1 9 5 4 11 12 2 8 6 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;数组逆序对构造&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/k-inverse-pairs-array/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;对于一个包含 $1$ 到 $n$ 的整数的数组，如果满足 $i &amp;lt; j$ 且 &lt;code&gt;nums[i] &amp;gt; nums[j]&lt;/code&gt; ，则称 &lt;code&gt;(i, j)&lt;/code&gt; 为一个 &lt;strong&gt;逆序对&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给定两个整数 $n$ 和 $k$ ，请返回满足恰好包含 $k$ 个逆序对的、长度为 $n$ 的数组的个数。由于答案可能很大，请返回答案对 $10^9 + 7$ 取模的结果。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 1000$&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的数组个数对 $10^9 + 7$ 取模的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法随笔】并查集的应用</title><link>https://xingguang641.com/posts/acm/acm-note/disjoint-set-union/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/disjoint-set-union/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;并查集等价类维护&lt;/h1&gt;
&lt;h2&gt;程序自动化分析&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1955&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;金字塔对齐问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc428/tasks/abc428_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;移除最多的石头&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;（中介并查集）&lt;/p&gt;
&lt;h2&gt;可激活最大点数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-points-activated-with-one-addition/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;（中介并查集）&lt;/p&gt;
&lt;h2&gt;又有一道好问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/79505/K&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;虚点技巧（中介并查集）&lt;/p&gt;
&lt;h2&gt;最大公约数遍历&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/greatest-common-divisor-traversal/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;GCD并查集（枚举因子）（中介并查集）&lt;/p&gt;
&lt;h2&gt;最大公因数排序&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/gcd-sort-of-an-array/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;GCD并查集（枚举因子）（中介并查集）&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;并查集连通性维护&lt;/h1&gt;
&lt;h2&gt;删除后的最大和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-segment-sum-after-removals/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;仅有删边的题目转成加边题（逆向思维）&lt;/p&gt;
&lt;h2&gt;情侣的牵手难题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/couples-holding-hands/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;贪心+并查集&lt;/p&gt;
&lt;h2&gt;新增道路查询后的最短距离问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/shortest-distance-after-road-addition-queries-ii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;区间并查集&lt;/p&gt;
&lt;h2&gt;二维矩阵集水器&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/kskhHQ/solutions/1876216/jiantu-by-endlesscheng-rkra/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;连通性维护（困难版）&lt;/p&gt;
&lt;h2&gt;判断角落可达性&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/check-if-the-rectangle-corner-is-reachable/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;连通性维护（困难版）&lt;/p&gt;
&lt;h2&gt;好路径的总个数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/number-of-good-paths/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;增量法+并查集&lt;/p&gt;
&lt;h2&gt;可获得的总分数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-points-from-grid-queries/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;增量法+并查集&lt;/p&gt;
&lt;h2&gt;动态边收缩问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc411/tasks/abc411_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;缩点+并查集&lt;/p&gt;
&lt;h2&gt;受限可达集问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc401/tasks/abc401_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;缩点+并查集&lt;/p&gt;
&lt;h2&gt;减少病毒的传播&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimize-malware-spread-ii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;缩点+并查集&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;并查集传递性维护&lt;/h1&gt;
&lt;p&gt;维护集合元素的相对关系&lt;/p&gt;
&lt;p&gt;种类并查集就是取模带权并查集&lt;/p&gt;
&lt;p&gt;扩展域并查集可以被种类并查集替代&lt;/p&gt;
&lt;h2&gt;推导部分和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8779&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;除法的求值问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/evaluate-division/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;异或带权并查集&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://acm.hdu.edu.cn/showproblem.php?pid=3234&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;增量偶权环查询&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/incremental-even-weighted-cycle-queries/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;银河英雄的传说&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1196&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;甄别食物链问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2024&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;带权并查集（种类并查集）、扩展域并查集&lt;/p&gt;
&lt;h2&gt;最小的冲突分配&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1525&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;扩展域并查集&lt;/p&gt;
&lt;h2&gt;敌人敌人是朋友&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1892&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;扩展域并查集&lt;/p&gt;
&lt;h2&gt;区间的奇偶问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P5937&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;真正的取模带权并查集，跟种类并查集和扩展域并查集有区别，但可以用扩展域并查集来做&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://oi-wiki.org/ds/dsu/&quot;&gt;【OI WiKi】并查集相关知识&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】贪心算法相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/heap-problems/greedy-algorithm/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/heap-problems/greedy-algorithm/</guid><description>记录一些 ACM 常见题型</description><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;子串拼接贪心问题&lt;/h1&gt;
&lt;p&gt;记录各种贪心排序&lt;/p&gt;
&lt;h2&gt;最大数构造问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-number/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一组非负整数 &lt;code&gt;nums&lt;/code&gt; ，重新排列每个数的顺序（每个数不可拆分）使之组成一个最大的整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：输出结果可能非常大，所以你需要返回一个字符串而不是整数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个非负整数，表示数组 $nums$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个字符串，表示组成的最大的整数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
10 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;210
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
3 30 34 5 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9534330
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;交叉组合排序&lt;/p&gt;
&lt;h2&gt;国王的奖赏游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1080&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;交叉组合排序&lt;/p&gt;
&lt;h2&gt;所需的最少能量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-initial-energy-to-finish-tasks/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个任务数组 $tasks$ ，其中 $tasks[i] = [actual_i, minimum_i]$ ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$actual_i$ 是完成第 $i$ 个任务需要耗费的能量。&lt;/li&gt;
&lt;li&gt;$minimum_i$ 是开始第 $i$ 个任务前需要具备的最少能量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如，如果任务为 $[10, 12]$ ，而你当前的能量为 $11$ ，那么你不能开始该任务。如果你当前的能量为 $13$ ，你可以开始该任务，且完成任务后剩余能量为 $3$ 。&lt;/p&gt;
&lt;p&gt;你可以按 &lt;strong&gt;任意顺序&lt;/strong&gt; 完成任务。请你返回完成所有任务所需的 &lt;strong&gt;最少&lt;/strong&gt; 初始能量。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq tasks.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq actual_i \leq minimum_i \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示任务的数量。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含两个整数，分别表示 $actual_i$ 和 $minimum_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$actual_1 \quad minimum_1$&lt;/p&gt;
&lt;p&gt;$actual_2 \quad minimum_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$actual_n \quad minimum_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示完成所有任务所需的最少初始能量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 3
2 4
10 11
10 12
8 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;32
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 7
2 8
3 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;13
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;差值贪心排序&lt;/p&gt;
&lt;h2&gt;知识竞赛的筹备&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/2a9089ea7e5b474fa8f688eae76bc050&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;部门要选两位员工参加知识竞赛。每个员工 $i$ 有两个能力值：推理能力 $A_i$ 和阅读能力 $B_i$ 。&lt;/p&gt;
&lt;p&gt;如果选择第 $i$ 个人和第 $j$ 个人组队，他们在竞赛中表现出的能力如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阅读能力&lt;/strong&gt;：$X = \frac{B_i + B_j}{2}$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推理能力&lt;/strong&gt;：$Y = \frac{A_i + A_j}{2}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在需要最大化他们表现较差一方面的能力，即让 $\min(X, Y)$ 尽可能大。请问这个最大值是多少？&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i, B_i \leq 10^8$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个正整数 $n$ ，代表员工数。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含两个正整数 $A_i$ 和 $B_i$ ，分别描述第 $i$ 个员工的推理能力和阅读能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$A_1 \quad B_1$&lt;/p&gt;
&lt;p&gt;$A_2 \quad B_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_n \quad B_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;仅输出一行，包含一个一位小数，表示 $\min(X, Y)$ 的最大值。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2 2
3 1
1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;最小值贪心排序/差值绝对值贪心排序（最小值贪心需要证明正确性，可以对拍）&lt;/p&gt;
&lt;h2&gt;消灭的怪物数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/eliminate-maximum-number-of-monsters/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你正在玩一款电子游戏，在游戏中你需要保护城市免受怪物的进攻。给你两个长度为 $n$ 的整数数组 $dist$ 和 $speed$ ，其中 $dist[i]$ 是第 $i$ 只怪物距离城市的初始距离，而 $speed[i]$ 是这只怪物每分钟向城市移动的速度。&lt;/p&gt;
&lt;p&gt;你在游戏开始时（第 $0$ 分钟）有一把武器，并已经蓄力完毕。你可以使用这把武器 &lt;strong&gt;瞬间&lt;/strong&gt; 消灭一只怪物。但是，武器每次使用后都需要 $1$ 分钟的时间进行再充电，在此期间你无法再次使用。&lt;/p&gt;
&lt;p&gt;当怪物的距离 &lt;strong&gt;小于或等于 0&lt;/strong&gt; 时，它就到达了城市，游戏结束。&lt;/p&gt;
&lt;p&gt;请返回在输掉游戏前，你最多能消灭的怪物数量。如果你可以在所有怪物到达城市前将它们全部消灭，返回 $n$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == dist.length == speed.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq dist[i], speed[i] \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示怪物的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 $dist$ 中的元素。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示数组 $speed$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$dist_0 \quad dist_1 \quad \ldots \quad dist_{n-1}$&lt;/p&gt;
&lt;p&gt;$speed_0 \quad speed_1 \quad \ldots \quad speed_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示你最多能消灭的怪物数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 3 4
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 1 2 3
1 1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;性价比排序&lt;/p&gt;
&lt;h2&gt;最低的雇佣成本&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $n$ 名工人。给定两个整数数组 $quality$ 和 $wage$ ，其中 $quality[i]$ 表示第 $i$ 名工人的工作质量，$wage[i]$ 表示第 $i$ 名工人的最低期望工资。&lt;/p&gt;
&lt;p&gt;现在我们想雇佣恰好 $k$ 名工人组成一个小组。在雇佣一组工人时，我们必须按照下述规则付费：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对小组内的每一名工人，应当按其工作质量与小组内其他工人的工作质量的比例来付工资。&lt;/li&gt;
&lt;li&gt;小组内每名工人的工资至少应当是其最低期望工资。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请返回支付这 $k$ 名工人的最低成本。与实际答案误差在 $10^{-5}$ 以内的结果将被视为正确。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == quality.length == wage.length$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n \leq 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq quality[i], wage[i] \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 $quality$ 中的元素。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示数组 $wage$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$quality_0 \quad quality_1 \quad \ldots \quad quality_{n-1}$&lt;/p&gt;
&lt;p&gt;$wage_0 \quad wage_1 \quad \ldots \quad wage_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个浮点数，表示最低总成本，保留五位小数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
10 20 5
70 50 30
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;105.00000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 3
3 1 10 10 1
4 8 2 2 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;30.66667
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;性价比排序&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;两地调度贪心问题&lt;/h1&gt;
&lt;p&gt;先全部选一个数组，然后再适当调整的贪心策略&lt;/p&gt;
&lt;h2&gt;两地的调度问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/two-city-scheduling/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;公司计划面试 $2n$ 名调度人员。给你一个数组 $costs$ ，其中 $costs[i] = [aCost_i, bCost_i]$ ，表示第 $i$ 人飞往 $A$ 市的费用为 $aCost_i$ ，飞往 $B$ 市的费用为 $bCost_i$ 。&lt;/p&gt;
&lt;p&gt;返回将每个人飞往其中一座城市的最低总费用，要求每个城市都有 $n$ 人抵达。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2n == costs.length$&lt;/li&gt;
&lt;li&gt;$2 \leq costs.length \leq 100$&lt;/li&gt;
&lt;li&gt;$costs.length$ 是偶数&lt;/li&gt;
&lt;li&gt;$1 \leq aCost_i, bCost_i \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $2n$ ，表示调度人员的总数。&lt;/li&gt;
&lt;li&gt;接下来的 $2n$ 行，每行包含两个整数，分别表示 $aCost_i$ 和 $bCost_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$2n$&lt;/p&gt;
&lt;p&gt;$aCost_1 \quad bCost_1$&lt;/p&gt;
&lt;p&gt;$aCost_2 \quad bCost_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$aCost_{2n} \quad bCost_{2n}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最低总费用。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
10 20
30 200
400 50
30 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;110
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
259 770
448 54
926 667
184 139
840 118
577 469
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1859
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最大异或和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/gym/675909/problem/K&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;整数拆分贪心问题&lt;/h1&gt;
&lt;p&gt;整数拆分动态规划是计算拆分的不同方法数，整数拆分贪心是使得最终的累乘积最大&lt;/p&gt;
&lt;h2&gt;竹子的最大价值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/jian-sheng-zi-ii-lcof/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;现需要将一根长为正整数 &lt;code&gt;bamboo_len&lt;/code&gt; 的竹子砍为若干段，每段长度均为 &lt;strong&gt;正整数&lt;/strong&gt; 。请返回每段竹子长度的 &lt;strong&gt;最大乘积&lt;/strong&gt; 是多少。&lt;/p&gt;
&lt;p&gt;由于答案可能很大，请将结果对 $10^9 + 7$ 取模。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最大乘积对 $10^9 + 7$ 取模后的值。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;81
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;拆分的最大乘积&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class090/Code02_MaximumProduct.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个正整数 $n$ ，将其拆分为 &lt;strong&gt;恰好 k 个&lt;/strong&gt; 正整数，使得这 $k$ 个整数的乘积最大。&lt;/p&gt;
&lt;p&gt;由于结果可能非常大，请将最终结果对 $10^9 + 7$ 取模。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq k \leq n \leq 10^{12}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最大乘积对 $10^9 + 7$ 取模后的值。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;整数均摊贪心问题&lt;/h1&gt;
&lt;h2&gt;平均值最小总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class091/Code04_SplitMinimumAverageSum.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $n$ 的数组 &lt;code&gt;arr&lt;/code&gt; 和一个正整数 $k$ 。现需要将 &lt;code&gt;arr&lt;/code&gt; 划分为 $k$ 个集合，使得数组中的每个数字恰好进入一个集合。&lt;/p&gt;
&lt;p&gt;请计算并返回这 $k$ 个集合各自平均值的累加和的最小值。每个集合的平均值计算方式为：该集合内所有元素的总和除以元素个数，结果 &lt;strong&gt;向下取整&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq arr[i] \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，分别表示数组长度和需要划分的集合数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 $arr$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$arr_1 \quad arr_2 \quad \ldots \quad arr_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有集合平均值累加和的最小值。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;小船过河贪心问题&lt;/h1&gt;
&lt;h2&gt;双人船渡河问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1809&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在月黑风高的夜晚，$n$ 个人来到河边，准备借助仅有的一盏灯过河。由于河水湍急，每次最多只能有两人同时过河，且过河时必须携带灯。&lt;/p&gt;
&lt;p&gt;已知每个人单独过河所需的时间，若两人同时过河，其所需时间取决于较慢的那个人。由于灯只有一盏，每次两人过河后，必须有一人将灯送回对岸，以便其他人过河。&lt;/p&gt;
&lt;p&gt;请计算出全员过河所需的最短总时间。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;每个人的过河时间为不超过 $10^6$ 的正整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示总人数。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行包含一个整数，表示第 $i$ 个人过河所需的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$t_1$&lt;/p&gt;
&lt;p&gt;$t_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$t_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1
2
5
10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;17
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;樵夫伐木贪心问题&lt;/h1&gt;
&lt;h2&gt;梦幻城的黄金树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class094/Code05_CuttingTree.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在梦幻城市中有 $n$ 棵黄金树，每棵树每天都会结出金子。已知第 $i$ 棵树初始时已有 $a_i$ 个金币，且每天会新长出 $b_i$ 个金币。&lt;/p&gt;
&lt;p&gt;JAVAMAN 准备在梦幻城市停留 $m$ 天，每天他只能选择砍掉一棵树并获得该树上所有的金币。需要注意的是，如果某一天他不砍树，那么在那之后的日子里他也无法再砍树。&lt;/p&gt;
&lt;p&gt;请计算他在 $m$ 天内最多可以获得的金币总数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 200$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq n \leq 250$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq b_i \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示每棵树初始的金币数 $a_i$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示每棵树每天增长的金币数 $b_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多可以获得的金币总数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
2 1
10 10
1 1
2 2
8 10
2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
21
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最优的烹调方案&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1417&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;由于美食节将至，店主希望在 $t$ 时间内做出一些美味佳肴。现有 $n$ 件食材，每件食材有三个属性：$a_i$、$b_i$ 和 $c_i$ 。&lt;/p&gt;
&lt;p&gt;如果在第 $j$ 时刻完成第 $i$ 件食材的烹调，可以获得的美味度为：&lt;/p&gt;
&lt;p&gt;$$
a_i - j \times b_i
$$&lt;/p&gt;
&lt;p&gt;每件食材烹调所需的时间为 $c_i$ 。请问如何安排烹调顺序，才能使获得的美味度总和最大。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 50$&lt;/li&gt;
&lt;li&gt;$1 \leq t \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i, b_i, c_i \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含四行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $t$ 和 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示 $a_1, a_2, \ldots, a_n$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示 $b_1, b_2, \ldots, b_n$ 。&lt;/li&gt;
&lt;li&gt;第四行包含 $n$ 个整数，表示 $c_1, c_2, \ldots, c_n$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$t \quad n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_n$&lt;/p&gt;
&lt;p&gt;$c_1 \quad c_2 \quad \ldots \quad c_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最大美味度总和。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;74 1
502
2
47
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;408
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;我们一起来打CF&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/aqjndtsb&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;田忌赛马贪心问题&lt;/h1&gt;
&lt;p&gt;解锁任务型贪心&lt;/p&gt;
&lt;h2&gt;复杂版田忌赛马&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1650&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;中国古代的历史上，齐王和田忌赛马的故事家喻户晓。齐王和田忌各有 $N$ 匹马，每匹马都有一个固定的速度值。&lt;/p&gt;
&lt;p&gt;比赛规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一轮双方各出一匹马进行比赛，每匹马只能使用一次，直到 $N$ 匹马全部赛完。&lt;/li&gt;
&lt;li&gt;在一轮比赛中，如果田忌的马速度大于齐王的马，田忌获胜，得 $200$ 银币。&lt;/li&gt;
&lt;li&gt;如果田忌的马速度小于齐王的马，田忌失败，扣除 $200$ 银币。&lt;/li&gt;
&lt;li&gt;如果两匹马速度相等，则是平局，不奖励也不扣除银币。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;田忌已知齐王出马的顺序。请你通过合理安排田忌出马的顺序，使得田忌最终能获得的银币总数最大。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 2000$&lt;/li&gt;
&lt;li&gt;马的速度不超过 $1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示马的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示田忌的 $N$ 匹马的速度。&lt;/li&gt;
&lt;li&gt;第三行包含 $N$ 个整数，表示齐王的 $N$ 匹马的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_N$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示田忌能获得的最大银币数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
92 83 71
95 87 74
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;200
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
20 20
20 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最新版田忌赛马&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/119605/D&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;田忌与齐王再次进行赛马比赛。这次比赛规则有所不同，田忌需要通过合理安排马匹的对阵顺序，最大化自己的赏金收益。&lt;/p&gt;
&lt;p&gt;田忌有 $n$ 匹马，第 $i$ 匹马的速度为 $a_i$ ；齐王有 $m$ 匹马，第 $i$ 匹马的速度为 $b_i$ 。由于田忌对自己和齐王的马匹了如指掌，他知道他和齐王的马都是按速度降序排列的。&lt;/p&gt;
&lt;p&gt;每次比赛，田忌可以选择自己的一匹从未出战的马 $i$ 与齐王的一匹从未出战的马 $j$ 进行比赛：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果田忌的马速度严格大于齐王的马，田忌将获得 $b_j$ 的赏金。&lt;/li&gt;
&lt;li&gt;如果田忌的马速度小于或等于齐王的马，田忌不会获得赏金。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要计算田忌能够获得的 &lt;strong&gt;最大&lt;/strong&gt; 赏金总额。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n, m \leq 5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i, b_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;数组 $a$ 和 $b$ 均已按 &lt;strong&gt;升序&lt;/strong&gt; 排列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ ，分别表示田忌和齐王的马匹数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示田忌各匹马的速度。&lt;/li&gt;
&lt;li&gt;第三行包含 $m$ 个整数，表示齐王各匹马的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示田忌能够获得的最大赏金总额。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
3 1
4 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
11 10 7
10 9 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;19
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 7
6 5 4 3 2 1
7 6 5 4 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;加强版田忌赛马&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/119605/E&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;田忌与齐王再次进行赛马比赛。这次比赛规则有所不同，田忌需要通过合理安排马匹的对阵顺序，最大化自己的赏金收益。&lt;/p&gt;
&lt;p&gt;田忌有 $n$ 匹马，第 $i$ 匹马的速度为 $a_i$ ；齐王有 $m$ 匹马，第 $i$ 匹马的速度为 $b_i$ 。由于田忌对自己和齐王的马匹了如指掌，他知道他和齐王的马都是按速度降序排列的。&lt;/p&gt;
&lt;p&gt;每次比赛，田忌可以选择自己的一匹从未出战的马 $i$ 与齐王的一匹从未出战的马 $j$ 进行比赛：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果田忌的马速度严格大于齐王的马，田忌将获得 $b_j$ 的赏金。&lt;/li&gt;
&lt;li&gt;如果田忌的马速度小于或等于齐王的马，田忌不会获得赏金。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要计算田忌能够获得的 &lt;strong&gt;最大&lt;/strong&gt; 赏金总额，以及有多少种 &lt;strong&gt;本质不同&lt;/strong&gt; 的对阵方案可以获得该最大赏金总额。&lt;/p&gt;
&lt;p&gt;两种对阵策略被称为本质不同的，当且仅当其中一个对阵策略中，存在某个田忌的马 $i$ 与齐王的马 $j$ 比赛，而另一个对阵策略中，田忌的马 $i$ 不与齐王的马 $j$ 比赛。&lt;/p&gt;
&lt;p&gt;由于对阵策略种类数可能很多，对阵策略种类数只需要计算对 $998244353$ 取模后的答案，但是注意不要对最大赏金总额取模。&lt;/p&gt;
&lt;p&gt;注意：你不需要最大化比赛数量，不进行任何比赛也是对阵策略的一种。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n, m \leq 5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i, b_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;数组 $a$ 和 $b$ 均已按 &lt;strong&gt;降序&lt;/strong&gt; 排列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ ，分别表示田忌和齐王的马匹数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示田忌各匹马的速度。&lt;/li&gt;
&lt;li&gt;第三行包含 $m$ 个整数，表示齐王各匹马的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出两个整数，第一个整数表示田忌能够获得的最大赏金总额，第二个整数表示有多少种本质不同的对阵策略可以获得该最大赏金总额，其中对阵策略种类数需要对 $998244353$ 取模。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
3 1
4 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;大规模募资问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/ipo/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;假设 LeetCode 即将开始 IPO。为了以更高的价格将股票卖给风险投资公司，LeetCode 希望在 IPO 之前开展一些项目以增加其资本。由于资源有限，它只能在 IPO 之前完成最多 $k$ 个不同的项目。帮助 LeetCode 设计完成最多 $k$ 个指定的项目后，可获得的最大总资本。&lt;/p&gt;
&lt;p&gt;给你 $n$ 个项目。对于每个项目 $i$ ，它都有一个纯利润 $profits[i]$ ，和启动该项目需要的最小资本 $capital[i]$ 。&lt;/p&gt;
&lt;p&gt;最初，你的资本为 $w$ 。当你完成一个项目时，你将获得纯利润，且利润将被添加到你的总资本中。&lt;/p&gt;
&lt;p&gt;总而言之，从给定项目中选择最多 $k$ 个不同项目的列表，以 &lt;strong&gt;最大化最终资本&lt;/strong&gt; ，并输出最终可获得的最多资本。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq k \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq w \leq 10^9$&lt;/li&gt;
&lt;li&gt;$n == profits.length$&lt;/li&gt;
&lt;li&gt;$n == capital.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq profits[i] \leq 10^4$&lt;/li&gt;
&lt;li&gt;$0 \leq capital[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含四行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，分别表示项目的数量和最多可选择的项目数量。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $w$ ，表示初始资本。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示每个项目的纯利润 $profits$ 。&lt;/li&gt;
&lt;li&gt;第四行包含 $n$ 个整数，表示每个项目启动所需的最小资本 $capital$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$w$&lt;/p&gt;
&lt;p&gt;$profits_1 \quad profits_2 \quad \ldots \quad profits_n$&lt;/p&gt;
&lt;p&gt;$capital_1 \quad capital_2 \quad \ldots \quad capital_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最终可获得的最大总资本。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
0
1 2 3
0 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
0
1 2 3
0 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最低的加油次数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-refueling-stops/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;汽车从起点出发驶向目的地，该目的地位于距起点 &lt;code&gt;target&lt;/code&gt; 英里处。&lt;/p&gt;
&lt;p&gt;沿途有若干个加油站，每个 &lt;code&gt;station[i]&lt;/code&gt; 代表一个加油站，它位于距起点 $station[i][0]$ 英里处，并且有 $station[i][1]$ 升汽油。&lt;/p&gt;
&lt;p&gt;假设汽车离起点距离无限远且油箱容量无限大，初始时燃料为 &lt;code&gt;startFuel&lt;/code&gt; 升。它每行驶 $1$ 英里就会用掉 $1$ 升汽油。当汽车到达一个加油站时，它可能停下来加油，将加油站所有的汽油都装入油箱。&lt;/p&gt;
&lt;p&gt;为了到达目的地，汽车最少需要加油多少次？如果无法到达目的地，则返回 $-1$ 。&lt;/p&gt;
&lt;p&gt;注意：如果汽车到达目的地时剩余燃料为 $0$ ，它仍然被视为到达了目的地。如果它到达加油站时剩余燃料为 $0$ ，它仍然可以在该加油站加油。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq target, startFuel \leq 10^9$&lt;/li&gt;
&lt;li&gt;$0 \leq stations.length \leq 500$&lt;/li&gt;
&lt;li&gt;$0 &amp;lt; station[i][0] &amp;lt; station[i+1][0] &amp;lt; target$&lt;/li&gt;
&lt;li&gt;$1 \leq station[i][1] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $target$ 和 $startFuel$ ，分别表示目的地的距离和初始燃料量。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $N$ ，表示加油站的数量。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行包含两个整数 $dist_i$ 和 $fuel_i$ ，表示第 $i$ 个加油站距离起点的距离和拥有的燃料量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$target \quad startFuel$&lt;/p&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$dist_1 \quad fuel_1$&lt;/p&gt;
&lt;p&gt;$dist_2 \quad fuel_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$dist_N \quad fuel_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最少需要的加油次数。如果无法到达，输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;100 1
1
10 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;100 10
4
10 60
20 30
30 30
60 40
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;数组同化贪心问题&lt;/h1&gt;
&lt;p&gt;中位数贪心相关&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1922938031687595039&quot;&gt;中位数贪心及其证明&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;使数组元素相等&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;构造模交替数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-operations-to-make-array-modulo-alternating-i/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;建筑抢修贪心问题&lt;/h1&gt;
&lt;p&gt;经典反悔贪心之一&lt;/p&gt;
&lt;h2&gt;建筑抢修小游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4053&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;小修正在赶往一些建筑进行抢修。由于建筑物的受损程度不同，抢修每个建筑所需的时间以及该建筑能够支撑的最晚抢修时间也各不相同。&lt;/p&gt;
&lt;p&gt;具体而言，第 $i$ 个建筑抢修需要花费的时间为 $T_1$ ，而它在 $T_2$ 时刻之后就会倒塌。如果小修决定抢修某个建筑，他必须在 &lt;strong&gt;该建筑倒塌之前&lt;/strong&gt; 完成抢修。&lt;/p&gt;
&lt;p&gt;小修从 $0$ 时刻出发，每次只能抢修一个建筑。请你帮助小修进行规划，使得他能够抢修的建筑数量最多。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 1.5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq T_1 \leq T_2 &amp;lt; 2^{31}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示建筑的数量。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行包含两个整数 $T_1$ 和 $T_2$ ，分别表示抢修该建筑需要的时间和该建筑的最晚倒塌时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$T_{1,1} \quad T_{2,1}$&lt;/p&gt;
&lt;p&gt;$T_{1,2} \quad T_{2,2}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$T_{1,N} \quad T_{2,N}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多可以抢修的建筑数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
100 200
200 1300
1000 1250
2000 3200
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;城市绿化贪心问题&lt;/h1&gt;
&lt;h2&gt;复杂的种树问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1792&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在一条环形街道旁共有 $N$ 棵树。为了美化环境，需要在其中选择 $M$ 棵树种上装饰物。&lt;/p&gt;
&lt;p&gt;种树需要遵守以下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任意两棵相邻的树不能同时被种上装饰物。&lt;/li&gt;
&lt;li&gt;由于是环形街道，第 $1$ 棵树与第 $N$ 棵树被视为相邻。&lt;/li&gt;
&lt;li&gt;每棵树都有一个美观度 $a_i$ ，你的目标是使得选出的 $M$ 棵树的总美观度最大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果无法按照规则种下 $M$ 棵树，则输出 &lt;code&gt;Error!&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq M \leq N$&lt;/li&gt;
&lt;li&gt;$-1000 \leq a_i \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，分别表示树的总数和需要种装饰物的树的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示每棵树的美观度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最大总美观度。如果方案不存在，输出 &lt;code&gt;Error!&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 3
1 2 3 4 5 6 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 4
1 2 3 4 5 6 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Error!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2301_79248256/article/details/155039748&quot;&gt;【CSDN 博客】贪心算法之区间问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/hwrxooq5&quot;&gt;【Luogu 博客】反悔贪心的再理解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/wshcl/p/18712932&quot;&gt;【wshcl】反悔贪心相关题目收集&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】MOD相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/mod-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/mod-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MOD两数之和问题&lt;/h1&gt;
&lt;p&gt;在处理涉及模运算的加法问题时，一个核心的观察在于对操作数取值范围的精确控制。当我们预先对 $a$ 和 $b$ 分别执行 $a \pmod M$ 与 $b \pmod M$ 的标准化操作后，这两个操作数均被约束在半开区间 $[0, M)$ 之内。此时，它们的算术和 $a + b$ 的取值范围必然处于 $[0, 2M - 2]$ 之间。这一性质直接简化了取模运算的逻辑分支：&lt;/p&gt;
&lt;p&gt;由于 $a + b$ 最大也不会达到 $2M$ ，因此 $(a + b) \pmod M$ 的结果只存在两种线性可能。若 $a + b$ 小于 $M$ ，则其模运算结果即为原和本身；若 $a + b$ 落在 $[M, 2M - 2]$ 之间，则等价于从和中减去一个周期的偏移量。这种分类讨论通常被表述为：&lt;/p&gt;
&lt;p&gt;$$
(a + b) \pmod M =
\begin{cases}
a + b, &amp;amp; a + b &amp;lt; M \
a + b - M, &amp;amp; a + b \geq M
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;这一结论在算法优化中具有极高的实用价值。它不仅规避了计算机底层的除法/取模指令（这些指令通常比加减法慢数倍），还为诸如 &lt;strong&gt;双指针&lt;/strong&gt; 或 &lt;strong&gt;二分查找&lt;/strong&gt; 解决 “两数之和模 $M$ 的最大值” 等问题奠定了理论基础。通过将复杂的余数分布简化为简单的线性平移，我们能够更直观地在同余系下维护数值的单调性，从而将原本 $O(N^2)$ 的暴力搜索通过排序与双指针技巧优化至 $O(N \log N)$ 。&lt;/p&gt;
&lt;h2&gt;取模累加和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc416/tasks/abc416_d&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;二小姐取数问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/119225/E&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;整除子序列问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class071/Code02_MaxSumDividedBy7.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/math-operators/mod-problem/congruence/&quot;&gt;【ACM 算法题单】同余原理相关问题&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】有效括号问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-problems/regular-bracket/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-problems/regular-bracket/</guid><description>记录一些 ACM 常见题型</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;有效括号基础问题&lt;/h1&gt;
&lt;p&gt;有效括号问题是一个非常经典的入门题型，它的核心在于刻画括号之间的 &lt;strong&gt;匹配关系&lt;/strong&gt; 。一个合法的括号串，本质上由两种结构组成：第一种是 &lt;strong&gt;嵌套结构&lt;/strong&gt; ，即括号内部还包含完整的括号序列；第二种是 &lt;strong&gt;并列结构&lt;/strong&gt; ，即多个合法括号序列首尾相接。这两种结构的存在，使得我们在处理括号匹配问题时需要一种能够 “记住最近未匹配状态” 的数据结构，而栈正好满足这一需求。&lt;/p&gt;
&lt;p&gt;我们可以将整个过程抽象为一种动态匹配过程：每遇到一个左括号，就相当于产生了一个待匹配项，需要在之后被某个右括号消去；而每遇到一个右括号，则尝试去匹配最近的一个尚未匹配的左括号。因此，我们可以将左括号视为入栈操作，将右括号视为出栈操作，从而用栈来模拟整个匹配过程。&lt;/p&gt;
&lt;p&gt;在具体编写代码时，可以从非法情况入手理解整个过程。首先，如果在某一时刻遇到右括号，但此时栈为空，说明当前没有任何左括号可以与之匹配，这种情况称为 &lt;strong&gt;右括号多余&lt;/strong&gt; ，显然是不合法的。其次，即使在遍历过程中始终没有出现上述情况，如果在遍历结束后栈中仍然存在元素，则说明还有左括号未被匹配，这种情况称为 &lt;strong&gt;左括号多余&lt;/strong&gt; ，同样不合法。根据上面的思路，我们就可以写出如下的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
string s;

int main(){
    cin &amp;gt;&amp;gt; s;

    stack&amp;lt;char&amp;gt; st;
    for (char c : s){
        if (c == &apos;(&apos;){
            st.push(c);
        } else if (c == &apos;)&apos;){
            if (st.empty()){
                cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
                return 0;
            }
            st.pop();
        }
    }

    if (st.empty()){
        cout &amp;lt;&amp;lt; &quot;Valid&quot; &amp;lt;&amp;lt; endl;
    } else {
        cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个代码同样可以扩展到 &lt;strong&gt;包含多种类型括号&lt;/strong&gt; 的情形，例如 &lt;code&gt;()&lt;/code&gt; 、&lt;code&gt;[]&lt;/code&gt; 、&lt;code&gt;{}&lt;/code&gt; 等。整体思路并没有改变：左括号入栈，右括号尝试与栈顶元素匹配。不同之处在于，我们需要额外判断括号类型是否对应，如果栈为空或类型不匹配，则说明当前括号串不合法。因此，在实现时只需为每种括号建立对应关系即可，当遇到右括号时检查其是否与栈顶左括号匹配，匹配成功则出栈，否则直接判定为非法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
string s;

int main(){
    cin &amp;gt;&amp;gt; s;

    stack&amp;lt;char&amp;gt; st;
    for (char c : s){
        if (c == &apos;(&apos; || c == &apos;[&apos; || c == &apos;{&apos;){
            st.push(c);
        } else {
            if (st.empty()){
                cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
                return 0;
            }

            char t = st.top();
            st.pop();

            if ((c == &apos;)&apos; &amp;amp;&amp;amp; t != &apos;(&apos;) ||
                (c == &apos;]&apos; &amp;amp;&amp;amp; t != &apos;[&apos;) ||
                (c == &apos;}&apos; &amp;amp;&amp;amp; t != &apos;{&apos;)){
                cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
                return 0;
            }
        }
    }

    if (st.empty()) cout &amp;lt;&amp;lt; &quot;Valid&quot; &amp;lt;&amp;lt; endl;
    else cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过在算法竞赛中，大多数题目仍然以 &lt;strong&gt;单一类型括号&lt;/strong&gt; 为主。对于这类问题，我们没有必要使用栈来维护匹配关系，因为括号的类型是唯一的，我们只需要关心当前未匹配的左括号数量即可。因此可以用一个变量 &lt;code&gt;bal&lt;/code&gt; 来代替栈：遇到左括号时令 &lt;code&gt;bal++&lt;/code&gt; ，遇到右括号时令 &lt;code&gt;bal--&lt;/code&gt; 。在遍历过程中必须始终保证 &lt;code&gt;bal&lt;/code&gt; 不会小于 $0$（否则说明出现了多余的右括号），并且在遍历结束后 &lt;code&gt;bal&lt;/code&gt; 恰好等于 $0$ ，才能说明整个括号串是合法的。基于这一思路，可以将原本的栈做法优化为更简洁的计数实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
string s;

int main(){
    cin &amp;gt;&amp;gt; s;

    int bal = 0;
    for (char c : s){
        if (c == &apos;(&apos;){
            bal++;
        } else if (c == &apos;)&apos;){
            bal--;
            if (bal &amp;lt; 0){
                cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
                return 0;
            }
        }
    }

    if (bal == 0) cout &amp;lt;&amp;lt; &quot;Valid&quot; &amp;lt;&amp;lt; endl;
    else cout &amp;lt;&amp;lt; &quot;Invalid&quot; &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种基于 &lt;code&gt;balance&lt;/code&gt; 的处理方式具有很强的可拓展性，许多看似复杂的括号问题，本质上都可以转化为对这一变量的约束与维护。例如在 “判断任意区间是否为有效括号” 的问题中，我们不再关心整体是否合法，而是关注某一段区间的结构是否满足条件。这时可以将括号二值化（如左括号记为 $+1$ ，右括号记为 $-1$ ），从而把问题转化为区间和的性质分析。&lt;/p&gt;
&lt;h2&gt;最有价值的括号&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc407/tasks/abc407_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $2N$ 的非负整数序列 $A = (A_1, A_2, \ldots, A_{2N})$ 。&lt;/p&gt;
&lt;p&gt;对于一个长度为 $2N$ 的括号序列 $s$ ，定义其 &lt;strong&gt;得分&lt;/strong&gt; 如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于所有满足 $s_i = \texttt{&apos;)&apos;}$ 的位置 $i$ ，将 $A_i$ 的值改为 $0$ ；&lt;/li&gt;
&lt;li&gt;然后计算整个序列 $A$ 的元素总和。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，只有被选为 &lt;code&gt;&apos;(&apos;&lt;/code&gt; 的位置对应的 $A_i$ 会被计入答案。&lt;/p&gt;
&lt;p&gt;现在要求你在所有 &lt;strong&gt;合法括号序列&lt;/strong&gt; 中，选择一个，使得得分最大，并输出这个最大值。&lt;/p&gt;
&lt;p&gt;给定 $T$ 个测试用例，请分别求解。&lt;/p&gt;
&lt;p&gt;一个合法括号序列的定义是：可以通过不断删除子串 &lt;code&gt;&quot;()&quot;&lt;/code&gt; ，最终变为空串的字符串。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 500$&lt;/li&gt;
&lt;li&gt;$1 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq A_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有测试用例中 $N$ 的总和不超过 $2 \times 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$case_1$&lt;/p&gt;
&lt;p&gt;$case_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$case_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $2N$ 行，每行包含一个整数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$A_1$&lt;/p&gt;
&lt;p&gt;$A_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_{2N}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $T$ 行，第 $i$ 行表示第 $i$ 个测试用例的答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
3
400
500
200
100
300
600
6
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
1000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1200
6000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;对于这类结构复杂的有效括号问题，一个自然的切入点是从区间结构出发进行动态规划。由于合法括号序列本身具有明显的递归性质，一个完整序列可以表示为下面这个形式：&lt;/p&gt;
&lt;p&gt;$$
\big( , \text{left} , \big) , \text{right}
$$&lt;/p&gt;
&lt;p&gt;我们可以尝试固定第一个左括号的位置，并枚举其匹配的右括号，从而将原问题划分为左右两个相互独立的子区间，形成典型的 “枚举划分点” 区间 DP 模型。由于在计算过程中需要对所有可能的匹配点进行枚举，该算法的复杂度高达 $O(n^3)$ ，即便进行一定的优化，其性能依然难以满足本题的数据要求，因此该思路在本题中并不可行。&lt;/p&gt;
&lt;p&gt;既然从结构划分的角度难以突破，我们就换一个视角，从合法括号的判定条件入手。根据前面提到的 &lt;code&gt;balance&lt;/code&gt; 思想，一个合法括号序列在任意前缀上都必须满足 &lt;code&gt;balance ≥ 0&lt;/code&gt; 。将这一条件转化到本题中，可以理解为，在前 $i$ 个位置中，被选择为左括号的数量必须不少于 $\lceil i/2 \rceil$ ，否则一定会出现前缀失衡的情况。因此问题可以转化为一个带有前缀约束的选择问题，我们需要在满足约束的前提下，使被选中的元素权值之和尽可能大。&lt;/p&gt;
&lt;p&gt;基于这一点，我们可以采用贪心策略来构造答案。随着位置 $i$ 从左到右推进，每当需要保证前缀合法性时，本质上就是 &lt;strong&gt;每经过两个位置，就必须新增一个左括号&lt;/strong&gt; 。此时我们在当前已经出现的元素中选择一个权值最大的作为左括号，可以借助优先队列维护这些候选元素，每次在需要补充时取出最大值加入答案。通过这种方式，在保证所有前缀始终合法的同时，也尽可能让贡献最大的元素被选中，从而得到全局最优解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAX = 4e5 + 200;
int T, N;
int a[MAX];

int main(){
    cin &amp;gt;&amp;gt; T;
    while(T--){
        cin &amp;gt;&amp;gt; N;
        for (int i = 0; i &amp;lt; 2 * N; i++){
            cin &amp;gt;&amp;gt; a[i];
        }

        ll ans = a[0];
        priority_queue&amp;lt;int&amp;gt; pq;
        for (int i = 0; i &amp;lt; N - 1; i++){
            pq.push(a[2 * i + 1]);
            pq.push(a[2 * i + 2]);
            ans += pq.top(); pq.pop();
        }

        cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;有效括号计数问题&lt;/h1&gt;
&lt;p&gt;有效括号计数是算法竞赛中非常经典且常见的一类问题。它不仅涉及基础的字符串处理技巧，还常常与动态规划、栈以及组合数学等方法密切相关。理解这一类问题的核心，对于掌握算法思维和提升代码能力都有很大的帮助。在这一类问题中，我们通常可以将它们分为两种主要类型：&lt;strong&gt;有效括号子串计数&lt;/strong&gt; 和 &lt;strong&gt;有效括号序列计数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这两类问题虽然名称相近，但在解题逻辑上却存在显著差异。前者本质上属于 &lt;strong&gt;字符串匹配问题&lt;/strong&gt; ，重点在于分析原始字符串中哪些片段能够构成合法的括号配对，通常需要借助栈结构或动态规划来实时维护匹配状态；后者则属于典型的 &lt;strong&gt;组合数学问题&lt;/strong&gt; ，核心在于依据约束条件计算所有合法序列的总数，这类问题通常采用递归、动态规划或卡塔兰数等数学公式进行推导，无需针对具体字符串进行逐一遍历。&lt;/p&gt;
&lt;h3&gt;最长有效括号子串&lt;/h3&gt;
&lt;p&gt;在深入分析有效括号子串计数问题之前，可以先从 &lt;strong&gt;最长有效括号子串&lt;/strong&gt; 的问题入手，这有助于理解处理括号匹配的一些关键思想。有效括号问题中有一个非常重要的思路是 &lt;strong&gt;balance 概念&lt;/strong&gt;：任意前缀中左括号的数量不能少于右括号的数量。当某个前缀的 &lt;code&gt;balance&lt;/code&gt; 小于 $0$ 时，说明从该位置开始到当前位置的括号序列无法形成有效子串，这个位置可以作为一个 &lt;strong&gt;分割点&lt;/strong&gt; 。利用这一性质，可以有效划分字符串并进行匹配计算。&lt;/p&gt;
&lt;p&gt;具体实现时，可以在栈中先放入一个 &lt;strong&gt;哨兵元素 -1&lt;/strong&gt; ，表示初始的分割点位置。随后按照常规的括号匹配方法遍历字符串，但需要对栈操作稍作修改：判断栈的大小而不是是否为空。当栈的大小仅剩 $1$ 时，说明当前的右括号对应的位置也是一个分割点，此时用当前下标更新栈底元素。通过这种方式，就可以正确地记录有效括号子串的起点和长度，从而求出最长有效括号子串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
string s;

int main() {
    cin &amp;gt;&amp;gt; s;

    stack&amp;lt;int&amp;gt; stk;
    stk.push(-1); int ans = 0;
    for (int i = 0; i &amp;lt; s.size(); i++) {
        if (s[i] == &apos;(&apos;) {
            stk.push(i);
        } else if (stk.size() &amp;gt; 1) {
            stk.pop();
            ans = max(ans, i - stk.top());
        } else {
            stk.top() = i;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有效括号问题基本上也可以使用动态规划来解决，因此我们可以深入思考一下如何用 DP 来处理这个题目。对于子串类型的问题，有一个非常常见的状态设计方法，就是定义 &lt;code&gt;dp[i]&lt;/code&gt; 来表示以下标 $i$ 结尾的子串的状态。具体到最长有效括号问题，我们自然可以定义 &lt;code&gt;dp[i]&lt;/code&gt; 表示 &lt;strong&gt;以索引 i 结尾的最长有效括号子串的长度&lt;/strong&gt; ，即从字符串起始位置到下标 $i$ ，以 $i$ 为右端点的最长有效子串长度。&lt;/p&gt;
&lt;p&gt;很显然，如果一个子串以左括号结尾，它不可能形成有效括号，因此在遇到左括号时，我们直接跳过这个位置，并将所有左括号对应位置的 &lt;code&gt;dp[i]&lt;/code&gt; 设置为 $0$ 。右括号的处理才是关键。当一个右括号的前一个字符是左括号时，这意味着当前右括号就是于这个左括号直接匹配，形成一对有效括号。但此时 &lt;strong&gt;不能简单地认为以&lt;/strong&gt; 该右括号结尾的有效括号子串就只包括这一对括号，因为它前面可能已经存在一个完整的有效括号子串等待与当前匹配的这一对括号并列。换句话说，我们不仅要考虑这一对括号本身，还要将它之前的有效括号长度累加到当前长度中，从而正确得到以 $i$ 结尾的最长有效括号子串长度。&lt;/p&gt;
&lt;p&gt;当右括号的前一个字符也是右括号时，情况就更复杂了，此时我们需要找到与当前右括号匹配的左括号。显然，这个匹配的左括号下标为 &lt;code&gt;j = i - dp[i-1] - 1&lt;/code&gt; ，其中 &lt;code&gt;dp[i-1]&lt;/code&gt; 正是中间包含的有效括号子串的长度。匹配到左括号之后，我们得到了一段新的有效括号子串，但同样不能忽略左括号左边可能存在的完整有效括号子串，它们同样会与当前子串并列，因此同样要把这部分长度累加进来。通过这种方式，动态规划可以同时处理 &lt;strong&gt;有效括号的嵌套结构&lt;/strong&gt;（右括号匹配前一个左括号或中间子串）以及 &lt;strong&gt;并列结构&lt;/strong&gt;（前面的有效括号与当前匹配子串累加），从而在遍历整个字符串后得到每个下标结尾的最长有效括号子串长度，保证了完整性和准确性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 2e4 + 100;
int dp[MAXN];
string s;

int main() {
    cin &amp;gt;&amp;gt; s;

    int ans = 0;
    for (int i = 0; i &amp;lt; (int) s.size(); i++) {
        if (s[i] == &apos;(&apos;) continue;
        if (i &amp;gt; 0 &amp;amp;&amp;amp; s[i - 1] == &apos;(&apos;) {
            dp[i] = 2 + (i &amp;gt;= 2 ? dp[i - 2] : 0);
        } else if (i &amp;gt; 0 &amp;amp;&amp;amp; s[i - 1] == &apos;)&apos;) {
            int j = i - dp[i - 1] - 1;
            if (j &amp;gt;= 0 &amp;amp;&amp;amp; s[j] == &apos;(&apos;) {
                dp[i] = dp[i - 1] + 2 + (j - 1 &amp;gt;= 0 ? dp[j - 1] : 0);
            }
        }
        ans = max(ans, dp[i]);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;有效括号子串计数&lt;/h3&gt;
&lt;p&gt;有效括号子串计数问题可以分为两类：一类是 &lt;strong&gt;只要位置不同的有效括号就算一个子串&lt;/strong&gt; ，另一类是 &lt;strong&gt;统计本质不同的有效括号子串&lt;/strong&gt; 。前者关注的是 &lt;strong&gt;每个连续子串的位置组合&lt;/strong&gt; ，只要左右括号位置不同就算作不同的子串；后者关注的是 &lt;strong&gt;子串本身的内容是否不同&lt;/strong&gt; ，即相同内容的子串只算一次。由于两类问题的计算方式和复杂度差异较大，这里我们先专注于 &lt;strong&gt;第一类问题&lt;/strong&gt; ，即统计所有位置不重复的有效括号子串。&lt;/p&gt;
&lt;p&gt;解决这个问题的思路可以借鉴 &lt;strong&gt;最长有效括号子串&lt;/strong&gt; 的动态规划方法，但需要把重点从子串长度转移到子串数量。我们定义 &lt;code&gt;dp[i]&lt;/code&gt; 表示 &lt;strong&gt;以下标 i 结尾的有效括号子串数量&lt;/strong&gt; 。很显然，如果 &lt;code&gt;s[i]&lt;/code&gt; 是左括号，则不可能形成有效子串，因此将 &lt;code&gt;dp[i]&lt;/code&gt; 置为 $0$ 并跳过。当 &lt;code&gt;s[i]&lt;/code&gt; 是右括号时，需要找到与其匹配的左括号下标 $j$ 。&lt;/p&gt;
&lt;p&gt;我们可以用栈来优化匹配过程，栈中存储所有未匹配的左括号下标。当遇到右括号时，弹出栈顶左括号 $j$ ，形成一对新的有效括号子串。这里新增的子串数量为 $1$ ，表示当前匹配的这对括号本身。如果没有匹配的左括号，说明以当前右括号结尾不存在有效括号子串，同样直接跳过。&lt;/p&gt;
&lt;p&gt;此外，我们还需要考虑有效括号中的 &lt;strong&gt;并列结构&lt;/strong&gt;：如果左括号 $j$ 的左边存在已统计的有效括号子串，那么这些子串可以与当前新匹配的括号并列，形成更多的有效括号子串。因此，我们需要将左边的子串数量累加到当前计数中，得到以当前右括号结尾的总有效括号子串数量：&lt;/p&gt;
&lt;p&gt;$$
dp[i] = 1 + (j - 1 \geq 0 , ? , dp[j - 1] : 0)
$$&lt;/p&gt;
&lt;p&gt;遍历完字符串后，把每个 &lt;code&gt;dp[i]&lt;/code&gt; 累加到全局答案 &lt;code&gt;ans&lt;/code&gt; ，即可得到整个字符串中 &lt;strong&gt;所有位置不重复的有效括号子串总数&lt;/strong&gt; 。这种方法能够同时处理嵌套和并列结构，保证每个右括号结尾的新有效子串都被正确统计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 2e4 + 100;
int dp[MAXN];
string s;

int main() {
    cin &amp;gt;&amp;gt; s;

    stack&amp;lt;int&amp;gt; stk;
    int ans = 0;
    for (int i = 0; i &amp;lt; (int) s.size(); i++) {
        if (s[i] == &apos;(&apos;) {
            stk.push(i);
        }
        else if (!stk.empty()) {
            int j = stk.top(); stk.pop();
            dp[i] = 1 + (j - 1 &amp;gt;= 0 ? dp[j - 1] : 0);
            ans += dp[i];
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（另一个问题需要用到 SAM 去重，暂时不写）&lt;/p&gt;
&lt;h3&gt;最长有效括号序列&lt;/h3&gt;
&lt;p&gt;为了更好地理解 &lt;strong&gt;有效括号序列计数问题&lt;/strong&gt; ，我们可以先从 &lt;strong&gt;最长有效括号序列&lt;/strong&gt; 入手。与子串问题不同，序列问题的约束更加宽松，因为 &lt;strong&gt;不要求连续&lt;/strong&gt; ，只需要保持原本的顺序即可。因此，一个很自然的贪心策略就是：&lt;strong&gt;只要遇到可以匹配的右括号，就立即匹配&lt;/strong&gt; 。对于无法匹配的右括号，直接忽略即可。&lt;/p&gt;
&lt;p&gt;为什么这个贪心策略是正确的呢？因为有效括号序列的长度增长依赖于 &lt;strong&gt;左括号能找到匹配的右括号&lt;/strong&gt; 。一旦遇到右括号，如果当前有可匹配的左括号，立即匹配不会亏损长度；如果当前没有多余的左括号，这个右括号本身无法做出任何贡献，直接跳过即可。换句话说，在子序列问题中，多余的右括号不会成为序列的分割点阻碍后续的匹配。因此我们只需用贪心策略匹配尽可能多的括号对，就能得到最长有效括号序列的长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
string s;

int main() {
    cin &amp;gt;&amp;gt; s;

    int left = 0;
    int ans = 0;
    for (char c : s) {
        if (c == &apos;(&apos;) {
            left++;
        } else if (c == &apos;)&apos; &amp;amp;&amp;amp; left &amp;gt; 0) {
            ans += 2;
            left--;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种思路是使用区间动态规划。&lt;strong&gt;区间 DP 与有效括号问题有很大的联系&lt;/strong&gt; ，许多复杂的有效括号序列问题都可以通过区间 DP 得到启发。虽然对于这个问题而言，最优解并不是区间 DP，但这种方法仍然具有很强的启发意义，能够为解决更复杂的有效括号序列问题提供思路。&lt;/p&gt;
&lt;p&gt;具体来说，我们可以考虑当前区间的 &lt;strong&gt;左端点 L 对应的左括号&lt;/strong&gt; 应该与哪个右括号匹配。通过这种方式，可以将整个区间的结构拆解为下面这个形式：&lt;/p&gt;
&lt;p&gt;$$
\big( , \text{left} , \big) , \text{right}
$$&lt;/p&gt;
&lt;p&gt;在此基础上，我们将区间 $[L, R]$ 划分为两个部分：第一个括号及其内部的有效区间 $[L+1, k-1]$ ，以及配对括号后的剩余区间 $[k+1, R]$ 。这种划分方式将原区间拆解为互不重叠的子结构，使得我们可以通过对子区间状态的组合运算得到最终答案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAX = 1e3;
int dp[MAX][MAX];
string s;

int main() {
    cin &amp;gt;&amp;gt; s;

    for (int len = 2; len &amp;lt;= (int) s.size(); len++) {
        for (int i = 0; i + len - 1 &amp;lt; n; i++) {
            int j = i + len - 1;
            if (s[i] == &apos;(&apos; &amp;amp;&amp;amp; s[j] == &apos;)&apos;) {
                dp[i][j] = dp[i+1][j-1] + 2;
            }
            for (int k = i; k &amp;lt; j; k++) {
                dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j]);
            }
        }
    }

    cout &amp;lt;&amp;lt; dp[0][n-1] &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;有效括号序列计数&lt;/h3&gt;
&lt;p&gt;与有效括号子串计数问题类似，有效括号序列计数问题也可以分为两类：一类是统计 &lt;strong&gt;所有位置不同的有效括号序列&lt;/strong&gt; ，即使括号序列的内容相同，但其在原字符串中的位置不同，也视为不同的有效括号序列；另一类是统计 &lt;strong&gt;本质不同的有效括号序列&lt;/strong&gt; ，也就是相同内容的有效括号序列只统计一次。这里我们先讨论第一类问题，也就是统计所有位置不同的有效括号序列数量。&lt;/p&gt;
&lt;p&gt;我们可以使用 &lt;strong&gt;区间 DP&lt;/strong&gt; 来解决这一问题，对于任意区间 $[i, j]$ ，可以从该区间的左右端点括号是否匹配的角度来思考。如果左右端点不匹配（不管实际上能否匹配），则可以利用容斥原理，将整个区间拆解为较小的子区间进行处理，从而计算其有效括号序列的数量。计算公式如下：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1]
$$&lt;/p&gt;
&lt;p&gt;当左右端点恰好能够匹配时，这对首尾括号不仅能够独立闭合，直接形成一个最简的有效序列，同时它们还可以作为一层外壳，包裹住内部区间 $[i+1, j-1]$ 中已经存在的每一个合法子序列，使这些原有的序列在嵌套之后演变为全新且更长的有效方案。因此我们需要在容斥原理计算出的基础值之上，额外累加这部分新增的贡献。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAX = 1e3 + 100;
const int MOD = 1e9 + 7;
ll dp[MAX][MAX];
string s;

int main() {
    cin &amp;gt;&amp;gt; s

    for (int len = 2; len &amp;lt;= (int) s.size(); len++) {
        for (int i = 0; i + len - 1 &amp;lt; n; i++) {
            int j = i + len - 1;

            dp[i][j] = (dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1] + MOD) % MOD;

            if (s[i] == &apos;(&apos; &amp;amp;&amp;amp; s[j] == &apos;)&apos;) {
                dp[i][j] = (dp[i][j] + dp[i + 1][j - 1] + 1) % MOD;
            }
        }
    }

    cout &amp;lt;&amp;lt; dp[0][n - 1] &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们考虑 &lt;strong&gt;本质不同的有效括号序列&lt;/strong&gt; 问题。所谓 “本质不同” ，指的是括号序列的结构不同，而不考虑括号序列在原字符串中的具体位置。因此在设计具体的动态规划方案时，我们需要构建一个 &lt;strong&gt;与位置无关&lt;/strong&gt; 的思路，而不能单纯依赖具体的区间或索引来定义状态。&lt;/p&gt;
&lt;p&gt;在处理这类问题时，一个常用的策略是：&lt;strong&gt;假设在所有可能的选择中，总是优先选择最靠左的那一个括号&lt;/strong&gt; 。举例来说，对于字符串 &lt;code&gt;&quot;(()))&quot;&lt;/code&gt; ，如果我们考虑子序列 &lt;code&gt;&quot;(())&quot;&lt;/code&gt; ，它的第二个右括号可以对应原字符串的第四个或第五个字符，但按照这个策略，我们总是优先选择第四个字符作为右括号。通过这个设定，我们就能在枚举组合时避免重复计数，从而确保每个有效括号序列只被计算一次。&lt;/p&gt;
&lt;p&gt;引入这个 “最靠左优先” 的策略之后，传统的区间 DP 方法就难以直接应用。因为区间 DP 通常依赖于固定的区间边界，而无法表达 “在多个可选位置中优先选择最左侧” 的逻辑。为此，我们可以借用前面提到的 &lt;code&gt;balance&lt;/code&gt; 思想来设计 DP 状态：定义 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示在处理到字符串第 $i$ 个字符时，当前左括号数量比右括号多 $j$ 个的有效序列数量，其中 $j$ 就是当前的 &lt;code&gt;balance&lt;/code&gt; 值。这样 DP 的状态就不依赖于具体位置，从而满足本质不同的要求。&lt;/p&gt;
&lt;p&gt;为了方便状态转移，我们可以先预处理出每个位置的下一个左括号和下一个右括号的位置，生成两个数组 &lt;code&gt;nextL&lt;/code&gt; 和 &lt;code&gt;nextR&lt;/code&gt; 。在 DP 转移时，如果选择添加一个左括号，就从 &lt;code&gt;nextL[i]&lt;/code&gt; 找到下一个可用的左括号，然后将 &lt;code&gt;dp[i][j]&lt;/code&gt; 累加到 &lt;code&gt;dp[nextL[i]][j + 1]&lt;/code&gt; 上；如果选择添加右括号，则从 &lt;code&gt;nextR[i]&lt;/code&gt; 找到下一个可用的右括号，将 &lt;code&gt;dp[i][j]&lt;/code&gt; 累加到 &lt;code&gt;dp[nextR[i]][j - 1]&lt;/code&gt; 上，但前提是 &lt;code&gt;j &amp;gt; 0&lt;/code&gt; ，也就是当前有足够的左括号与之匹配。&lt;/p&gt;
&lt;p&gt;最终，我们只需要累加所有 &lt;code&gt;dp[i][0]&lt;/code&gt; 的值，就可以得到整个字符串中所有本质不同的有效括号序列的数量。这个方法通过结合 &lt;code&gt;balance&lt;/code&gt; 和 “最左优先” 的策略，既避免了重复计数，又实现了与位置无关的精确统计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAX = 1e3 + 100;
const int MOD = 1e9 + 7;
ll dp[MAX][MAX];
int nxtL[MAX], nxtR[MAX];
string s;

int main() {
    cin &amp;gt;&amp;gt; s; int n = s.size();

    // 为了方便起始跳转，我们从一个虚拟的起点开始考虑
    int lastL = -1, lastR = -1;
    vector&amp;lt;int&amp;gt; nL(n + 1, -1), nR(n + 1, -1);
    for (int i = n - 1; i &amp;gt;= 0; i--) {
        if (s[i] == &apos;(&apos;) lastL = i;
        if (s[i] == &apos;)&apos;) lastR = i;
        nL[i] = lastL;
        nR[i] = lastR;
    }

    // 初始状态：从原串最左侧尝试开启序列
    if (nL[0] != -1) {
        dp[nL[0]][1] = 1;
    }

    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt;= n; j++) {
            if (dp[i][j] == 0) continue;

            // 尝试添加左括号：跳到 i 之后的第一个 &apos;(&apos;
            if (i + 1 &amp;lt; n &amp;amp;&amp;amp; nL[i + 1] != -1) {
                int p = nL[i + 1];
                dp[p][j + 1] = (dp[p][j + 1] + dp[i][j]) % MOD;
            }

            // 尝试添加右括号：前提是 j &amp;gt; 0，跳到 i 之后的第一个 &apos;)&apos;
            if (j &amp;gt; 0 &amp;amp;&amp;amp; i + 1 &amp;lt; n &amp;amp;&amp;amp; nR[i + 1] != -1) {
                int q = nR[i + 1];
                dp[q][j - 1] = (dp[q][j - 1] + dp[i][j]) % MOD;
            }
        }
    }

    ll ans = 0;
    for (int i = 0; i &amp;lt; n; i++) {
        ans = (ans + dp[i][0]) % MOD;
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;子序列括号得分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/contest/2191/problem/D2&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;对于一个括号序列 $t$ ，定义其 &lt;strong&gt;score&lt;/strong&gt; 如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $t$ 不是一个合法括号序列（Regular Bracket Sequence, RBS），则其分数为 $0$ 。&lt;/li&gt;
&lt;li&gt;如果存在一个合法括号序列 $r$ ，且 $r$ 比 $t$ 更好，则其分数为所有满足条件的 $r$ 中 $|r|$ 的最大值。&lt;/li&gt;
&lt;li&gt;否则，分数为 $0$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若括号序列 $a$ 比 $b$ 更好，当且仅当满足以下条件之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$b$ 是 $a$ 的前缀，且 $a \neq b$ 。&lt;/li&gt;
&lt;li&gt;令 $i$ 为第一个 $a_i \neq b_i$ 的位置，则 $a_i = \text{&apos;(&apos;}$ 且 $b_i = \text{&apos;)&apos;}$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单来说，$t$ 的分数是所有优于 $t$ 的合法括号子序列中，长度最长的那个的长度。给定一个长度为 $n$ 的括号序列 $s$ ，求其所有非空子序列的分数之和，结果对 $998244353$ 取模。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq t \leq 30$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 100$&lt;/li&gt;
&lt;li&gt;所有测试用例中 $n$ 的总和不超过 $100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一行包含一个整数 $t$ ，表示测试用例的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每一组测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个长度为 $n$ 的字符串 $s$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$t$&lt;/p&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $t$ 行，每行一个整数，表示该测试用例中所有子序列的分数之和，结果对 $998244353$ 取模。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1
(
6
()()()
6
(())()
8
(())()()
22
()()())()()(()()()((()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
4
0
22
563070
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;有效括号区间问题&lt;/h1&gt;
&lt;p&gt;在处理 &lt;strong&gt;有效括号的区间问题&lt;/strong&gt; 时，传统的栈方法就不再适用了。原因有两点：首先，栈的做法依赖于从左到右的顺序处理括号，这不适合 &lt;strong&gt;在线查询&lt;/strong&gt; ；其次，栈本质上只能处理单个序列的匹配情况，而无法快速回答任意区间是否为有效括号序列。为此，我们需要重新回到前面提到的 &lt;code&gt;balance&lt;/code&gt; 思路来分析问题。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;balance&lt;/code&gt; 的视角下，区间 $[l, r]$ 要想构成有效括号序列，当且仅当满足以下两个条件：首先是区间两端的 &lt;code&gt;balance&lt;/code&gt; 值必须相同，即 &lt;code&gt;balance[l-1] = balance[r]&lt;/code&gt; ，这保证了整个区间括号数匹配，使得序列整体达到平衡状态；其次是区间内部所有位置的 &lt;code&gt;balance&lt;/code&gt; 值都必须大于或等于两端的 &lt;code&gt;balance&lt;/code&gt; 值，也就是 &lt;strong&gt;区间内部的 balance 不能低于端点的 balance&lt;/strong&gt; ，这保证了括号不会出现右括号多余的非法情况。&lt;/p&gt;
&lt;p&gt;结合这两个条件，可以得到一个关键结论：&lt;strong&gt;区间的最小 balance 必须等于区间两端的 balance 值&lt;/strong&gt; 。换句话说，如果我们能快速查询某个区间 $[l, r]$ 内的最小 &lt;code&gt;balance&lt;/code&gt; ，就可以判断这个区间是否为有效括号区间。这正好启发我们使用 &lt;strong&gt;线段树&lt;/strong&gt; 来维护区间最小值，并且线段树还可以高效地处理区间最小值的查询和更新，从而满足大批量在线查询的需求。通过这种方法，我们就能够在 &lt;code&gt;balance&lt;/code&gt; 的框架下高效解决有效括号的区间问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
int balance[MAXN];
int seg[4 * MAXN];
string s;

// 构建线段树，维护区间最小值
void build(int node, int l, int r) {
    if (l == r) {
        seg[node] = balance[l];
        return;
    }
    int mid = (l + r) / 2;
    build(node*2, l, mid);
    build(node*2+1, mid+1, r);
    seg[node] = min(seg[node*2], seg[node*2+1]);
}

// 查询区间 [ql, qr] 的最小值
int query_min(int node, int l, int r, int ql, int qr) {
    if (qr &amp;lt; l || ql &amp;gt; r) return INT_MAX;
    if (ql &amp;lt;= l &amp;amp;&amp;amp; r &amp;lt;= qr) return seg[node];
    int mid = (l + r) / 2;
    return min(query_min(node*2, l, mid, ql, qr),
               query_min(node*2+1, mid+1, r, ql, qr));
}

// 判断区间 [l, r] 是否为有效括号区间
bool is_valid(int l, int r) {
    if (balance[l] != balance[r+1]) return false; // 两端 balance 必须相同
    int mn = query_min(1, 0, s.size(), l+1, r+1);
    return mn &amp;gt;= balance[l]; // 区间内最小 balance &amp;gt;= 左端点 balance
}

int main() {
    cin &amp;gt;&amp;gt; s;
    int n = s.size();
    balance[0] = 0;
    for (int i = 0; i &amp;lt; n; i++) {
        balance[i+1] = balance[i] + (s[i] == &apos;(&apos; ? 1 : -1);
    }

    // 构建线段树
    build(1, 0, n);
    vector&amp;lt;pair&amp;lt;int,int&amp;gt;&amp;gt; queries = { {0, n-1}, {0, 1}, {1, 4}, {2, 5} };
    for (auto q : queries) {
        int l = q.first, r = q.second;
        cout &amp;lt;&amp;lt; (is_valid(l,r) ? &quot;Yes&quot; : &quot;No&quot;) &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们在括号序列上增加 &lt;strong&gt;区间反转操作&lt;/strong&gt; ，上述基于 &lt;code&gt;balance&lt;/code&gt; + 线段树的方法就难以直接使用。原因在于，反转操作会改变区间内括号的顺序，从而使原本依赖左右端点 &lt;code&gt;balance&lt;/code&gt; 的判定逻辑失效。为了处理这种情况，我们需要重新思考括号区间的结构，寻找一种能够在 &lt;strong&gt;合并和反转操作下仍然保持正确性的表示方法&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;一种自然的思路是考虑 &lt;strong&gt;匹配括号的归约&lt;/strong&gt; 。对于任意一个括号串，如果我们不断地删除 &lt;code&gt;&apos;()&apos;&lt;/code&gt; 子串，最终剩下的部分一定是一个非常简单的结构：所有未匹配的右括号都在左侧，所有未匹配的左括号都在右侧。换句话说，经过完全匹配删除后的字符串一定是形如 &lt;code&gt;&quot;)))(((&quot;&lt;/code&gt; 的格式：左侧是若干 &lt;code&gt;&apos;)&apos;&lt;/code&gt; ，右侧是若干 &lt;code&gt;&apos;(&apos;&lt;/code&gt; 。这个观察提供了一种非常方便的表示方法：对于每个区间，我们只需要记录 &lt;strong&gt;左边剩余的右括号数量&lt;/strong&gt; 和 &lt;strong&gt;右边剩余的左括号数量&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在此基础上，区间的合并操作也变得自然。假设我们有两个相邻区间 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; ，各自记录了剩余右括号和剩余左括号。合并时，&lt;code&gt;A&lt;/code&gt; 区间右边的未匹配左括号和 &lt;code&gt;B&lt;/code&gt; 区间左边的未匹配右括号可以互相抵消，因为它们可以组成新的 &lt;code&gt;&apos;()&apos;&lt;/code&gt; 匹配对。抵消后的多余部分仍然作为新的未匹配括号加入合并后的区间表示中。通过这种方式，我们就能在合并时保持对区间未匹配括号的精确统计，而不依赖于原始顺序或具体位置。&lt;/p&gt;
&lt;p&gt;这个表示方法最大优点在于，它天然支持 &lt;strong&gt;区间反转操作&lt;/strong&gt; 。当一个区间被反转时，只需要交换 “左边的未匹配右括号” 和 “右边的未匹配左括号” 这两个统计值，就能正确反映反转后的状态，而不必重新计算整个区间 &lt;code&gt;balance&lt;/code&gt; 或最小值。这使得我们可以在 &lt;strong&gt;线段树&lt;/strong&gt; 中高效实现区间反转，同时仍然能够快速合并子区间或判断区间有效性。&lt;/p&gt;
&lt;p&gt;在此基础上，如果我们想要计算一个区间的 &lt;strong&gt;最长有效括号序列长度&lt;/strong&gt; ，方法也非常直观：假设区间长度为 &lt;code&gt;len&lt;/code&gt; ，左边剩余的右括号数量为 &lt;code&gt;L&lt;/code&gt; ，右边剩余的左括号数量为 &lt;code&gt;R&lt;/code&gt; ，那么区间内未匹配的括号总数就是 &lt;code&gt;L + R&lt;/code&gt; 。由于这些未匹配的括号无法构成有效匹配，其余的括号都能形成匹配对。因此，区间内的最长有效括号序列长度就是：&lt;/p&gt;
&lt;p&gt;$$
\text{maxValidLen} = \text{intervalLen} - (L + R)
$$&lt;/p&gt;
&lt;p&gt;这个公式非常简洁，而且可以在区间反转操作后快速更新，从而保证了我们可以在 $O(\log n)$ 的时间复杂度下维护和查询任意区间的最长有效括号序列长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://linlexiao.com/posts/a60cdb098078/&quot;&gt;【immix】用线段树解决括号序列问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com/article/xxxvyhgv&quot;&gt;【life_is_movie】有效括号序列问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】GCD相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/gcd-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/gcd-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;GCD单调收敛问题&lt;/h1&gt;
&lt;p&gt;在涉及区间或子数组的数论问题中，GCD 运算展现出一种极强的 &lt;strong&gt;单调收敛性&lt;/strong&gt; 。随着参与运算的元素数量增加，其结果呈现出只减不增的态势。从代数角度看，每当一个新元素加入运算，当前的 GCD 值要么保持不变，要么转化为原值的一个真因子。这种性质意味着，当我们以某个固定位置为起点向一侧扩展子数组时，其 GCD 值会形成一个递减的链条，并最终收敛于一个稳定状态。&lt;/p&gt;
&lt;p&gt;这一性质引申出一个核心结论：&lt;strong&gt;固定单一端点时，子数组的 GCD 种类极其有限&lt;/strong&gt; 。由于每次 GCD 值的改变都意味着至少失去一个质因子，一个数值为 $V$ 的元素在不断取 GCD 的过程中，其值发生变化的次数不会超过 $O(\log V)$ 次。因此，尽管长度为 $N$ 的序列拥有 $O(N^2)$ 个子数组，但若以 GCD 值进行状态归类，整个序列中本质不同的 &lt;strong&gt;GCD 状态数仅为 $O(N \log V)$&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这种信息的高度压缩性，使得 GCD 相关的区间问题往往可以从暴力枚举转化为 &lt;strong&gt;对状态跳变点的维护&lt;/strong&gt; 。在算法实现中，我们可以利用这种类似于取最小值运算的收缩性质，实时维护以当前位置为终点的所有不同 GCD 值及其对应的区间范围。由于状态数受限在对数级别，这种做法能将原本需要平方复杂度的统计问题，优化至近线性效率。&lt;/p&gt;
&lt;h2&gt;最大公约数变换&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/5500a375098947c482b2c4787057cb13?channelPut=w252acm&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;数组置一的次数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-operations-to-make-all-array-elements-equal-to-1/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;GCD更相减损问题&lt;/h1&gt;
&lt;h2&gt;加入差值的绝对值直到长度固定&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class090/Code06_AbsoluteValueAddToArray.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;GCD倍数枚举问题&lt;/h1&gt;
&lt;p&gt;在处理 GCD 相关的构造或计数问题中，一个极具启发性的思路是 &lt;strong&gt;围绕目标值反推可选元素集合&lt;/strong&gt; 。如果我们希望通过一组数的运算得到特定的公约数 $g$ ，最直观的观察是：所有参与运算的元素必须都是 $g$ 的倍数。这一简单的整除约束，实际上为我们圈定了所有潜在的候选对象。若一个数不是 $g$ 的倍数，它在任何包含它的集合中都会破坏 $g$ 作为公约数的可能性。&lt;/p&gt;
&lt;p&gt;基于这一逻辑，&lt;strong&gt;倍数枚举&lt;/strong&gt; 成为了验证目标值是否可行的核心手段。在实际建模时，我们可以通过枚举潜在的 GCD 值 $g$ ，并将原数组中所有 $g$ 的倍数提取出来进行整体 GCD 运算。如果这些倍数的最大公约数恰好等于 $g$ ，则说明该目标值在原数组中是可构造的；反之，若结果大于 $g$ ，则说明没有任何子集能精确收敛到 $g$ 。这种方法将复杂的子集搜索转化为了值域上的倍数扫描，利用类似埃氏筛的结构化枚举，大幅降低了状态处理的复杂度。&lt;/p&gt;
&lt;p&gt;这种从倍数关系入手的视角，同样是解决 LCM 问题的关键。由于 LCM 与 GCD 之间存在互补关系，LCM 的变化往往受到 GCD 取值的约束。相比于 LCM 在数值空间中极易发生不受控的倍数扩张，GCD 始终被限制在值域的约数结构之内。因此直接处理 LCM 往往难以控制搜索边界，而通过 &lt;strong&gt;枚举 GCD 来间接约束 LCM&lt;/strong&gt; 则显得更为高效。&lt;/p&gt;
&lt;h2&gt;多重集子集查询&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/16d153d47b334b0f8cc507b70a2e2473?channelPut=w252acm&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最小公倍数连通&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-connected-components-in-lcm-graph/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;GCD和式变换问题&lt;/h1&gt;
&lt;p&gt;在数论问题中，经常会遇到涉及两个变量的嵌套求和式。例如既要枚举 $i$ ，又要枚举 $j$ ，并且两者之间通过某种函数相结合。如果直接枚举所有数对，时间复杂度将达到 $O(n^2)$ ，在数据范围较大时无法接受。&lt;/p&gt;
&lt;p&gt;因此这类问题需要通过 &lt;strong&gt;和式变换&lt;/strong&gt; 来重新组织计算过程，其核心是利用&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/enumeration/#%E5%AF%B9%E8%B1%A1%E4%BA%A4%E6%8D%A2%E8%B4%A1%E7%8C%AE%E6%B3%95&quot;&gt;对象交换贡献法&lt;/a&gt;：我们不再逐个枚举变量所有可能的值，而是从 “某个计算结果出现了多少次” 的角度进行统计。&lt;/p&gt;
&lt;p&gt;以经典的 GCD 全局求和为例：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} \sum_{j=1}^{n} \gcd(i, j)
$$&lt;/p&gt;
&lt;p&gt;如果按照定义直接计算，那么需要枚举所有 $n^2$ 个数对。为了降低复杂度，可以换一种思路来求解这个式子：对于每一个可能的最大公约数 $k$ ，统计有多少对 $(i,j)$ 满足 $\gcd(i,j) = k$ ，然后乘上 $k$ 作为贡献。&lt;/p&gt;
&lt;p&gt;按照这个思路，可以把原来的求和改写为先枚举最大公约数的取值，再统计满足条件的数对数量：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} \sum_{j=1}^{n} \gcd(i, j) = \sum_{k=1}^{n} k \sum_{i=1}^{n} \sum_{j=1}^{n} \big[\gcd(i, j) = k\big]
$$&lt;/p&gt;
&lt;p&gt;接下来需要解决如何统计满足 $\gcd(i,j) = k$ 的数对数量。假设某一对数满足 $\gcd(i,j) = k$ ，那么 $i$ 和 $j$ 一定都包含因子 $k$ ，也就是说可以把它们写成 $i = kx$ 、$j = ky$ 。把这个形式代入 $\gcd(i,j)$ ，可以得到：&lt;/p&gt;
&lt;p&gt;$$
\gcd(i,j)=\gcd(kx,ky)=k\gcd(x,y)
$$&lt;/p&gt;
&lt;p&gt;为了让 $\gcd(i,j) = k$ 成立，就必须满足 $\gcd(x,y) = 1$ 。换句话说，当我们把 $i$ 和 $j$ 同时除以 $k$ 之后，得到的两个数必须是互质的。同时还需要考虑范围的变化：由于 $i \leq n$ 且 $j \leq n$ ，代入 $i = kx$ 、$j = ky$ 后可以得到 $x \leq \lfloor n/k \rfloor$ 、$y \leq \lfloor n/k \rfloor$ 。因此原本的问题，就转化为了在 $1$ 到 $\lfloor n/k \rfloor$ 的范围内统计互质数对：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^{n} k
\sum_{x=1}^{\lfloor n/k \rfloor}
\sum_{y=1}^{\lfloor n/k \rfloor}
\big[\gcd(x,y) = 1\big]
$$&lt;/p&gt;
&lt;p&gt;为了统计互质数对，可以利用欧拉函数的定义：欧拉函数 $\varphi(t)$ 表示在 $1$ 到 $t$ 的整数中，与 $t$ 互质的数的个数。也就是说，如果固定第二个数 $y = t$ ，那么满足 $\gcd(x,t) = 1$ 的 $x$ 一共有 $\varphi(t)$ 个。&lt;/p&gt;
&lt;p&gt;因此可以按第二个数来统计互质数对：对于 $y = 1, 2, 3, \ldots, m$ ，分别计算与它互质的 $x$ 的数量，然后全部加起来。如果只统计满足 $x \leq y$ 的数对，那么互质数对的数量就是：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{m} \varphi(i)
$$&lt;/p&gt;
&lt;p&gt;不过原问题中 $x$ 和 $y$ 是完全对称的：如果 $(x,y)$ 是一对互质数，那么 $(y,x)$ 也是一对互质数，因此每一个满足 $x&amp;lt;y$ 的数对都会对应一个对称的位置，只有对角线上的 $(1,1)$ 不会产生新的对称点。因此在整个 $m \times m$ 的网格中，互质数对的总数量可以写成：&lt;/p&gt;
&lt;p&gt;$$
2\sum_{i=1}^{m}\varphi(i)-1
$$&lt;/p&gt;
&lt;p&gt;这里乘 $2$ 是因为补上对称的那一半，而减去 $1$ 是因为 $(1,1)$ 被计算了两次。最后把 $m = \lfloor n/k \rfloor$ 代回原来的式子，就可以得到整个求和的最终形式：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^{n} k \left(2\sum_{i=1}^{\lfloor n/k \rfloor}\varphi(i)-1\right)
$$&lt;/p&gt;
&lt;p&gt;经过这一步变换之后，原本需要枚举所有数对的双重循环，就转化为了枚举 $k$ 的单层循环。在实际实现时，只需要用筛法预处理出欧拉函数，并计算它的前缀和，就可以高效地完成整个计算。&lt;/p&gt;
&lt;h3&gt;欧拉函数反演&lt;/h3&gt;
&lt;p&gt;在深入理解 GCD 和式变换后，我们可以将这一技巧推广为一种更具普适性的工具，即 &lt;strong&gt;欧拉反演&lt;/strong&gt; 。欧拉反演的核心思想是降维，它能够复杂的 $\gcd(i, j)$ 函数拆解，转化为对约数的枚举，从而简化计算逻辑。&lt;/p&gt;
&lt;p&gt;欧拉反演的理论基础源于欧拉函数的经典恒等式：&lt;/p&gt;
&lt;p&gt;$$
n = \sum_{d \mid n} \varphi(d)
$$&lt;/p&gt;
&lt;p&gt;该公式表明，任何正整数 $n$ 都可以表示为其所有约数的欧拉函数之和。当面对经典的 GCD 全局求和时，可以将 $\gcd(i, j)$ 带入欧拉恒等式中：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} \sum_{j=1}^{n} \gcd(i, j) = \sum_{i=1}^{n} \sum_{j=1}^{n} \sum_{d \mid \gcd(i, j)} \varphi(d)
$$&lt;/p&gt;
&lt;p&gt;关键步骤在于 &lt;strong&gt;交换求和顺序&lt;/strong&gt;：我们不再先枚举 $i$ 和 $j$ ，而是优先枚举因子 $d$ 。由于 $d \mid \gcd(i, j)$ 等价于 $d$ 同时整除 $i$ 和 $j$ ，在 $1$ 到 $n$ 的范围内，符合条件的 $i$ 的个数为 $\lfloor n/d \rfloor$ ，对应的 $j$ 也有 $\lfloor n/d \rfloor$ 个，因此每个 $\varphi(d)$ 的贡献次数为 $\lfloor n/d \rfloor^2$ 。最终可得公式：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} \sum_{j=1}^{n} \gcd(i, j) = \sum_{d=1}^{n} \varphi(d) \cdot \left\lfloor \frac{n}{d} \right\rfloor^2
$$&lt;/p&gt;
&lt;p&gt;这种推导在代数形式上更加简洁，避免了对称性处理中的繁琐细节。在算法实现上，结合线性筛预处理欧拉函数前缀和以及数论分块技术，可以将计算复杂度降低至 $O(\sqrt{n})$ ，适用于大规模数据计算。欧拉反演不仅是处理 GCD 和式问题的高效方法，也为深入理解数论反演（如莫比乌斯反演）奠定了基础。&lt;/p&gt;
&lt;h3&gt;莫比乌斯反演&lt;/h3&gt;
&lt;p&gt;在深入理解欧拉反演之后，我们进一步介绍数论中极为重要的工具，即 &lt;strong&gt;莫比乌斯反演&lt;/strong&gt; 。如果说欧拉反演是通过数值拆解来消除 GCD 的耦合关系，那么莫比乌斯反演则利用 &lt;strong&gt;容斥原理&lt;/strong&gt; 在因数空间上建立了一套精密的计数机制。它不仅能够处理 GCD 求和，更适用于带有 “恰好等于” 约束的计数问题，其核心在于利用 &lt;strong&gt;莫比乌斯函数 $\mu(d)$&lt;/strong&gt; 的性质，将示性函数转化为可枚举的求和形式。&lt;/p&gt;
&lt;p&gt;莫比乌斯反演的理论基础源于莫比乌斯函数的经典恒等式：&lt;/p&gt;
&lt;p&gt;$$
\sum_{d \mid n} \mu(d) = [n = 1]
$$&lt;/p&gt;
&lt;p&gt;该公式表明，只有当 $n = 1$ 时，所有因子的 $\mu$ 值之和为 $1$ ，否则为 $0$ 。在处理 GCD 问题时，这为我们去掉示性函数提供了严密的工具。当我们希望统计满足 $\gcd(i, j) = k$ 的数对时，可以先提取公因数 $k$ ，也就是将条件缩放为 $\big[\gcd(i/k, j/k) = 1\big]$ ，然后利用上述性质将条件逻辑转化为因子求和：&lt;/p&gt;
&lt;p&gt;$$
\Big[\gcd(\frac{i}{k}, \frac{j}{k}) = 1\Big] = \sum_{d \mid \gcd(i/k, j/k)} \mu(d)
$$&lt;/p&gt;
&lt;p&gt;接下来，通过 &lt;strong&gt;交换求和顺序&lt;/strong&gt; 将因子 $d$ 提到最外层，原本相互耦合的 $i$ 和 $j$ 变为 $kd$ 的倍数。在 $1$ 到 $n$ 的范围内，满足条件的 $i$ 和 $j$ 的个数均为 $\lfloor n/(kd) \rfloor$ 。这一变换的核心在于利用 $\mu(d)$ 的正负抵消性，将 “恰好互质” 的复杂统计转化为对 “公约数倍数” 的简单计数。最终，原本带有 GCD 约束的和式可重写为：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} \sum_{j=1}^{n} \big[\gcd(i, j) = k\big] = \sum_{d=1}^{\lfloor n/k \rfloor} \mu(d) \cdot \left\lfloor \frac{n}{kd} \right\rfloor^2
$$&lt;/p&gt;
&lt;p&gt;莫比乌斯反演的优势在于其高度通用性。与欧拉反演不同，它不依赖于 $n$ 可以拆解为约数和的特定性质，而是建立在更基础的 &lt;strong&gt;容斥逻辑&lt;/strong&gt; 上。这意味着无论是非对称求和范围 $n \neq m$ ，还是更复杂的倍数关系，莫比乌斯反演都能保持统一且标准的推导模式。在实际算法实现中，结合线性筛预处理莫比乌斯函数前缀和以及整除分块技术，莫比乌斯反演同样可以将计算复杂度降至亚线性量级，是处理高阶数论计数问题的重要工具。&lt;/p&gt;
&lt;h2&gt;互质的数对统计&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2568&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com/article/a2ddeqk4&quot;&gt;【Luogu 博客】GCD相关问题及其题解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/2003830361403171881&quot;&gt;【知乎博客】关于交换求和次序的一点思考&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/499839696&quot;&gt;【知乎博客】数论恐怖技巧：交换求和&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】ABS相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/abs-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/abs-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;ABS窗口约束问题&lt;/h1&gt;
&lt;p&gt;在处理涉及绝对值约束的问题时，一类经典的形式是要求集合内的元素满足 $|a_i - a_j| \leq M$ 。这个条件表面上是复杂的 &lt;strong&gt;全对约束&lt;/strong&gt; ，即要求任意两个元素之间都必须满足特定的差值关系，然而基于绝对值的几何特性，该条件可以等价地转化为对整体 &lt;strong&gt;极值&lt;/strong&gt; 的考察：&lt;/p&gt;
&lt;p&gt;$$
\max(a) - \min(a) \leq M
$$&lt;/p&gt;
&lt;p&gt;这一等价转化显著提升了算法处理的效率，它意味着我们不再需要逐一扫描 $O(N^2)$ 级别的元素对关系，而只需关注集合的边界，使得原本复杂的组合筛选问题得到了本质上的优化。&lt;/p&gt;
&lt;p&gt;从几何视角审视，这种差值约束可以被抽象为 &lt;strong&gt;区间覆盖模型&lt;/strong&gt; 。如果将每个元素 $a_i$ 看作以其为中心、半径为 $M/2$ 的闭区间 $[a_i - M/2, , a_i + M/2]$ ，那么原问题就等价于判定这些区间是否存在 &lt;strong&gt;公共交集&lt;/strong&gt; 。这一视角将数值间的距离限制转化为坐标轴上的几何覆盖，即寻找一个原点 $x$ ，使得所有选定的元素到 $x$ 的距离都不超过 $M/2$ 。&lt;/p&gt;
&lt;p&gt;从几何视角切换至极值视角也同样成立。例如假设每个元素 $a_i$ 可以在 $[a_i - k, , a_i + k]$ 内任意调整，我们需要判断是否存在某种方案使得最终所有元素达成一致，此时问题的本质是判断这些调整区间的交集是否非空。等价地，只需要对整体 &lt;strong&gt;极值&lt;/strong&gt; 进行考察：&lt;/p&gt;
&lt;p&gt;$$
\max(a) - \min(a) \leq 2k
$$&lt;/p&gt;
&lt;p&gt;在一些实际应用中，我们需要从序列中选取部分元素并保证其极差不超过 $M$ 。在已知某个元素 $L$ 必须被选中的前提下，我们可以围绕 $L$ 来分析可行解的范围。显然，所有可能与 $L$ 共存的元素必须落在 $[L - M, , L + M]$ 这一范围内。但这仅仅是必要条件，即便所有候选元素都落在此区间，最终选出的集合的极差也有可能超过 $M$ 。&lt;/p&gt;
&lt;p&gt;为了完整解决这个问题，可以采用 &lt;strong&gt;枚举合法窗口&lt;/strong&gt; 的思路：既然满足条件的集合必然落在某个长度为 $M$ 的区间内，我们可以枚举所有包含 $L$ 且总长度为 $M$ 的区间。这种方式将全局约束具象化为动态的区间筛选过程，从而方便地进行动态维护或在线处理。&lt;/p&gt;
&lt;h3&gt;整除/取模分类技巧&lt;/h3&gt;
&lt;p&gt;针对 $|a_i - a_j| &amp;lt; K$ 这一类 &lt;strong&gt;范围约束&lt;/strong&gt; 问题，&lt;strong&gt;整除分类&lt;/strong&gt;是一种极其高效的过滤手段。我们可以将数轴划分为长度为 $K$ 的若干个半开半闭区间，即对于任意元素 $a_i$ ，将其放入编号为 $\lfloor a_i / K \rfloor$ 的桶中。这种做法的精妙之处在于：如果两个元素的差值绝对值小于 $K$ ，它们要么落在 &lt;strong&gt;同一个桶&lt;/strong&gt; 中，要么落在 &lt;strong&gt;相邻的桶&lt;/strong&gt; 中。这一性质将原本需要 $O(N^2)$ 的全局比较，简化为仅需检查当前桶及左右邻居的局部搜索。&lt;/p&gt;
&lt;p&gt;而对于 $|a_i - a_j| \equiv 0 \pmod K$（即差值为 $K$ 的倍数）或者更具体的 $|a_i - a_j| = K$ 这种 &lt;strong&gt;精确间距约束&lt;/strong&gt; 问题，&lt;strong&gt;取模分类&lt;/strong&gt; 则是更直接的切入点。通过计算 $r_i = a_i \pmod K$ ，我们可以将全集划分为 $K$ 个互不相交的同余类。显而易见，属于不同余数的两个元素，其差值绝对不可能等于 $K$ 的整数倍。&lt;/p&gt;
&lt;p&gt;在每一个特定的同余桶内部，绝对值约束得到了极大的简化：原本要求的差值为 $K$ 的倍数，在数值除以 $K$ 后退化为寻找相邻的整数关系。这种策略不仅在静态计数问题中十分高效，在动态规划或组合优化场景下，也能作为状态压缩的前置步骤，通过缩减搜索空间显著提升算法性能。&lt;/p&gt;
&lt;h2&gt;数组最大美丽值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-beauty-of-an-array-after-applying-operation/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;部落的昂贵聘礼&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/U262078&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;禁忌的差值删除&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc403/tasks/abc403_d&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;取模分类&lt;/p&gt;
&lt;h2&gt;存在重复的元素&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/contains-duplicate-iii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;整除分类&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法题单】树上算法相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/tree-algorithms/tree-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/tree-algorithms/tree-algorithms/</guid><description>记录一些 ACM 常见题型</description><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树上点分治问题&lt;/h1&gt;
&lt;p&gt;在传统树型动态规划中，子树合并本质上是局部信息的逐层汇总。每个节点仅依赖其直接子节点提供的状态，通过既定的转移规则构造自身信息。只要这种合并能限制在对子节点状态的 &lt;strong&gt;简单组合&lt;/strong&gt; 内，而不需要显式展开子树内部的结构细节，单次合并的计算量便能得到有效控制，从而保证整体复杂度与树规模呈线性或近线性关系。&lt;/p&gt;
&lt;p&gt;然而，并非所有树上问题都能通过局部的状态汇总来解决。在路径统计或点对查询等模型中，我们往往需要处理跨越子树的 &lt;strong&gt;全量细节配对&lt;/strong&gt; 。这类问题的特征是：子树的信息无法被抽象为一个简单的数值，而是必须保留每一个节点相对于根节点的具体属性，例如距离或特征值。此时，合并操作不再是状态的简单叠加，而是演变为两个或多个 &lt;strong&gt;节点集合之间的全量交叉比对&lt;/strong&gt; 。由于这种操作必须触及子树内部的每一个细节，其计算代价与子树规模直接相关，导致合并本身成为了性能瓶颈。&lt;/p&gt;
&lt;p&gt;当这类高复杂度合并被嵌入传统递归框架时，算法性能将严重依赖于 &lt;strong&gt;子树规模的分布情况&lt;/strong&gt; 。普通树型 DP 遵循原图的拓扑结构，缺乏对递归划分的平衡性约束。在极端结构下，这种涉及全量集合比对的操作会在每一层递归中反复出现，导致时间复杂度发生不可控的退化。这种退化并非源于实现细节，而是因为传统递归结构无法限制细节枚举型操作的执行频率，从而产生了 &lt;strong&gt;内在的结构性缺陷&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;树上点分治正是通过引入 &lt;strong&gt;重心&lt;/strong&gt; 来重构递归逻辑，从而解决这一结构性问题的分治策略。它与普通树型 DP 的本质区别在于：它不再被动地跟随原树拓扑进行逐层合并，而是通过每一层选取重心作为分治点，&lt;strong&gt;强制将子问题的规模上界约束为原规模的一半&lt;/strong&gt; 。这种强制平衡将递归深度严格限制在 $O(\log n)$ 以内，确保了即便每一层都需要对子树集合进行高代价的交叉比对，这些操作也只会在对数级的层数中发生。通过这种方式，点分治将原本难以处理的集合比对问题，转化为了一系列受控的、层次分明的局部搜索过程。&lt;/p&gt;
&lt;p&gt;值得注意的是，树上点分治算法只适合用来解决 &lt;strong&gt;答案不随着根的改变而改变&lt;/strong&gt; 的题目。由于点分治的本质是通过不断变换重心来重构递归子树，这要求点对间的属性（如路径长度、权值最值）必须仅取决于两点间的唯一简单路径。如果问题的贡献逻辑（如 $LCA$ 相关）高度依赖于某个不可变更的全局原始根，频繁的换根操作就会彻底破坏属性的参考基准，导致分治失效。&lt;/p&gt;
&lt;h3&gt;动态点分治问题&lt;/h3&gt;
&lt;p&gt;点分树介绍&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;DFN序相关问题&lt;/h1&gt;
&lt;p&gt;在树结构的算法中，DFN 序（Depth First Numbering）指的是在进行一次深度优先搜索时，按照节点被第一次访问的顺序为每个节点分配的编号。具体来说，当 DFS 第一次访问到节点 $u$ 时，就给它分配一个递增的时间戳，这个编号称为该节点的 &lt;strong&gt;DFN 值&lt;/strong&gt; 。因此，DFN 序本质上就是 &lt;strong&gt;节点在 DFS 遍历过程中被访问的先后顺序&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在实现时，通常会维护一个全局计数器 &lt;code&gt;timer&lt;/code&gt; 。当 DFS 第一次进入节点 $u$ 时执行：&lt;/p&gt;
&lt;p&gt;$$
dfn[u] = ++timer
$$&lt;/p&gt;
&lt;p&gt;其中 $dfn[u]$ 表示节点 $u$ 是第 &lt;code&gt;timer&lt;/code&gt; 个被访问到的节点。由于 DFS 会先访问当前节点，再递归访问其所有子节点，因此 &lt;strong&gt;一个节点的整棵子树通常会被连续访问&lt;/strong&gt; 。如果记 $siz[u]$ 表示节点 $u$ 的子树大小，那么节点 $u$ 的子树在 DFN 序中往往对应一个连续区间：&lt;/p&gt;
&lt;p&gt;$$
\big[dfn[u], dfn[u] + siz[u] - 1\big]
$$&lt;/p&gt;
&lt;p&gt;这一性质使得我们可以把树结构中的 &lt;strong&gt;子树问题&lt;/strong&gt; 转化为 &lt;strong&gt;数组区间问题&lt;/strong&gt; 。从结构上看，DFN 序的核心作用其实是一种 &lt;strong&gt;树的线性化&lt;/strong&gt; 。原本的树是一个分支结构，而通过 DFS 编号之后，每个节点都会对应到一个一维数组的位置。这样一来，如果题目中需要对某个节点的 &lt;strong&gt;整棵子树进行统计或计算&lt;/strong&gt; ，就可以直接转化为对数组中某一段区间进行操作，然后我们还能借助 &lt;strong&gt;树状数组或线段树&lt;/strong&gt; 来维护区间信息，从而更高效地完成查询与更新。&lt;/p&gt;
&lt;p&gt;从更高层次的角度来看，DFN 序的关键作用在于 &lt;strong&gt;将树上的子树结构映射到一个连续的数组区间中&lt;/strong&gt; 。一旦完成这种映射，原本复杂的树结构问题就能够借助成熟的数组数据结构进行处理，从而大大简化算法设计。这也是 DFN 序在树上算法中被广泛使用的重要原因。&lt;/p&gt;
&lt;h2&gt;带修二叉树高度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/height-of-binary-tree-after-subtree-removal-queries/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;删边的最小代价&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-score-after-removals-on-a-tree/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;树上逆序对计数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3605&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;比较两树的权值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc406/tasks/abc406_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://taodaling.github.io/blog/2019/09/10/%E6%A0%91%E4%B8%8A%E7%AE%97%E6%B3%95/&quot;&gt;【Daltao】树上相关算法汇总&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】BIT相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/bit-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/bit-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;BIT逐位拆解问题&lt;/h1&gt;
&lt;p&gt;在许多涉及位运算的题目中，一个非常常见的思考方式是 &lt;strong&gt;逐位拆解&lt;/strong&gt; 。由于二进制表示天然具有独立性，一个整数的每一位往往可以单独分析，而不会与其他位产生复杂耦合。因此，在面对位运算问题时，第一步通常就是将问题从 “整数层面” 转化为 “二进制位层面” ，分别分析每一位的贡献。很多看似复杂的题目，一旦拆解到每一位之后，结构往往会变得非常清晰。&lt;/p&gt;
&lt;p&gt;位运算问题之所以适合逐位分析，是因为大多数位运算操作都具有 &lt;strong&gt;按位独立性&lt;/strong&gt; 。例如 &lt;code&gt;AND&lt;/code&gt; 、&lt;code&gt;OR&lt;/code&gt; 、&lt;code&gt;XOR&lt;/code&gt; 等运算操作，本质上都是对每一位分别进行计算。因此，如果题目中出现的是这些运算的组合，往往可以将问题拆解为：&lt;strong&gt;第 k 位最终会变成什么值&lt;/strong&gt; 。一旦我们能独立判断每一位的最优结果，就可以把所有位的结果重新组合得到答案。&lt;/p&gt;
&lt;h2&gt;位运算贪心构造&lt;/h2&gt;
&lt;p&gt;位运算贪心构造的核心，在于利用二进制权重的指数级分布特性进行确定性决策。这类题目主要分为两种题型。第一种涉及 &lt;strong&gt;元素修改或重组操作&lt;/strong&gt; ，其求解关键是从动态操作中抽象出静态的 &lt;strong&gt;固有贡献&lt;/strong&gt; 。题目提供的操作往往具有极强的方向性与不可逆性。例如，若操作为将元素 $a_i$ 与常数 $x$ 执行按位与（AND）运算，我们必须敏锐识别出位权空间的 &lt;strong&gt;不可触达区域&lt;/strong&gt;：如果常数 $x$ 的某位为 $1$ ，则该位在操作扰动下将始终保持初始状态，无法被实质性改变。这种由 &lt;strong&gt;算子属性&lt;/strong&gt; 决定的硬性约束，就是构造时必须预先锁定的 &lt;strong&gt;底层素材&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在识别并锁定固有贡献后，接下来的决策逻辑往往紧跟着针对剩余空间的 &lt;strong&gt;高位贪心&lt;/strong&gt; 。由于固有贡献属于无法更改的既定事实，我们应将其视为决策背景，而将贪心策略集中于那些 &lt;strong&gt;尚未被约束锁定、仍具备操作自由度&lt;/strong&gt; 的位信息上。基于 $2^i &amp;gt; \sum_{j=0}^{i-1} 2^j$ 的位权特性，高位 $1$ 的价值严格大于低位总和。因此，在排除固有贡献的干扰后，我们必须将剩余位资源视为最稀缺资产，通过 &lt;strong&gt;高位决策&lt;/strong&gt; 将其优先分配给能产生最大边际收益的位。这种在自由空间内实现的 &lt;strong&gt;位权压制&lt;/strong&gt; ，确保了我们在不触碰硬性约束的前提下，实现对全局目标函数上限的精准控制。&lt;/p&gt;
&lt;p&gt;第二种题型则涉及 &lt;strong&gt;不带修改的操作&lt;/strong&gt; ，通常要求从既定数组中选数以构造特定数值，其核心在于应用 &lt;strong&gt;高位贪心过滤机制&lt;/strong&gt; 。由于无法改变元素的内部位结构，决策过程被抽象为一种逐位的 &lt;strong&gt;存亡筛选&lt;/strong&gt; 。以 &lt;strong&gt;子集最大 OR&lt;/strong&gt; 为例，利用其 &lt;strong&gt;只增不减&lt;/strong&gt; 的特性，贪心策略通常趋于全集累加，以纳入所有可能的位贡献。而在处理 &lt;strong&gt;子集最大 AND&lt;/strong&gt; 时，基于其 &lt;strong&gt;只减不增&lt;/strong&gt; 的单调性，决策演变为严密的 &lt;strong&gt;排除法&lt;/strong&gt;：从最高位向低位逐一审视，若当前位为 $1$ 的元素集合足以支撑目标需求，则果断剔除在该位为 $0$ 的所有干扰元素。这种单向且不可逆的决策路径，利用高位结果为低位划定了更精确的搜索边界。&lt;/p&gt;
&lt;p&gt;然而，当这种基于单调性的高位决策逻辑碰撞上 &lt;strong&gt;异或（XOR）运算&lt;/strong&gt; 时，情况会发生质的变化。子集最大 XOR 问题无法简单地通过贡献重组或存亡过滤来解决。异或运算相同为 $0$ 且不同为 $1$ 的 &lt;strong&gt;自反特性&lt;/strong&gt; ，彻底破坏了数值增长的单调性。引入一个新元素可能会让原本已经确立的高位 $1$ 瞬间塌陷为 $0$ 。这种不确定性使得常规的贪心策略失效，迫使我们寻找更深层的 &lt;strong&gt;代数结构&lt;/strong&gt; 来支撑决策。&lt;/p&gt;
&lt;h3&gt;异或运算的按位贪心&lt;/h3&gt;
&lt;p&gt;从上面的分析可以知道，异或的贪心构造题往往比较复杂，其核心难点在于处理位与位之间的 &lt;strong&gt;相互耦合与干扰&lt;/strong&gt; 。在常规的 AND 或 OR 运算中，我们对某一位的操作通常不会产生导致高位结果反转的副作用。但在异或空间里，每一个元素的加入都可能引发全局数值的剧烈震荡。为了在缺乏单调性的环境中重新贯彻 &lt;strong&gt;高位优先&lt;/strong&gt; 的贪心原则，我们必须通过 &lt;strong&gt;人工构造&lt;/strong&gt; 的方式，将原本无序且相互制约的原始数据，转化为一套严密的、互不干扰的独立结构。&lt;/p&gt;
&lt;p&gt;这种结构化转化的本质是实现 &lt;strong&gt;决策解耦&lt;/strong&gt; 。我们需要确保在确定了最高位的最优解后，后续对低位的任何尝试和优化，都绝对不会对已经固定的高位产生任何负面影响。这种从 &lt;strong&gt;依赖数据天然属性&lt;/strong&gt; 到 &lt;strong&gt;人工构建结构化独立性&lt;/strong&gt; 的思想转变，是解决复杂异或最优化问题的通用范式。通过这种方式，我们将原本处于震荡中的动态决策，转化为一种在稳定结构上的线性搜索，从而在混沌中锁定每一位的高位收益。&lt;/p&gt;
&lt;h4&gt;1、线性基：子集最大异或和&lt;/h4&gt;
&lt;p&gt;在处理异或相关的最优化问题时，若直接面对包含 $n$ 个元素的原始集合 $S$ ，其子集异或组合的数量级将达到 $2^n$ 。当数据规模较大时，这种指数级的搜索空间会导致计算成本不可接受。为了高效处理这一空间，我们需要寻找一个 &lt;strong&gt;极小线性无关组&lt;/strong&gt;（即基底 $B$ ）。该基底必须满足两个核心特征：首先是 &lt;strong&gt;空间等效性&lt;/strong&gt; ，即原集合 $S$ 能产生的任何异或结果，均可由 $B$ 的子集异或得到；其次是 &lt;strong&gt;线性无关性&lt;/strong&gt; ，即 $B$ 中不存在任何可以通过其他元素组合得到的冗余元素。通过这种方式，我们将复杂的原始数据压缩为了一个规模仅为 $O(\log(\max S))$ 的精简集合，从而在根本上降低了后续决策的复杂度。&lt;/p&gt;
&lt;p&gt;以集合 $S = {12, 9, 14, 11}$ 为例，通过观察二进制位可以发现，$11$ 实际上可以由 $12 \oplus 9 \oplus 14$ 组合得到。这意味着在异或空间的维度上，$11$ 并没有提供任何超出 ${12, 9, 14}$ 范围的新信息。我们的目标是剔除这类 &lt;strong&gt;线性相关&lt;/strong&gt; 的冗余元素，找到能够支撑起相同空间的基底 $B = {12, 9, 14}$ 。然而，在实际计算中，直接从原数组中筛选出特定的子集往往效率低下且逻辑复杂。因此，我们采取 “曲线救国” 的策略：转而寻找一个与原集合 &lt;strong&gt;逻辑等价&lt;/strong&gt; 的集合 $B&apos;$ 。&lt;/p&gt;
&lt;p&gt;这里的 “等价” 基于两个核心代数推论：第一，用 $a \oplus b$ 的结果替代原集合中的 $a$ 或 $b$ ，不会改变集合所能张成的异或空间；第二，如果集合中存在 $a \oplus b = 0$ ，则说明其中一个元素是多余的，将其舍弃不会对非零异或和的组成产生任何影响。基于此，我们不必执着于保留原数组中的数字，而是可以通过不断的异或变换，构造出一组形态更加标准的等价元素。&lt;/p&gt;
&lt;p&gt;为了将上述等价变换转化为可执行的算法，我们引入了 &lt;strong&gt;高斯消元&lt;/strong&gt; 的思想。我们维护一个数组 $d$ ，其中 $d[i]$ 存储最高位在第 $i$ 位的基底。当新元素 $x$ 尝试进入线性基时，它会从最高位向低位逐一扫描。如果 $x$ 的第 $i$ 位为 $1$ 且 $d[i]$ 已经存在，则执行 $x = x \oplus d[i]$ 。这一步利用了替换不变性，旨在消除 $x$ 在该位上的影响力并继续向下探索。如果 $x$ 的最终结果被削减为 $0$ ，根据冗余舍弃性，我们将其视为无效信息丢弃；若它在某位停下并填补了空缺，则它成为了该位新的掌控者。&lt;/p&gt;
&lt;p&gt;这种通过消元得到的阶梯化集合 $B&apos;$ 具有极佳的 &lt;strong&gt;贪心特性&lt;/strong&gt; 。由于每个基底 $d[i]$ 的最高位被严格限制在第 $i$ 位，且其高位全部为 $0$ ，这就消除了不同元素之间的位权干扰。在求解最大异或和时，我们可以维护一个变量 $res$ ，并从高到低执行状态转移：&lt;/p&gt;
&lt;p&gt;$$
res = \max(res, res \oplus d[i])
$$&lt;/p&gt;
&lt;p&gt;这一步的数学依据非常严谨：如果 $res$ 的第 $i$ 位已经是 $1$ ，异或 $d[i]$ 必然会使该位变为 $0$ ，由于高位已经确定，这一步操作一定会让数值减小；反之，如果 $res$ 的第 $i$ 位是 $0$ ，异或 $d[i]$ 之后第 $i$ 位变为 $1$ ，无论低位如何变化，数值一定会增大。这种贪心策略之所以成立，本质上是因为二进制权值具有绝对的 &lt;strong&gt;高位压制特性&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
2^i &amp;gt; \sum_{j=0}^{i-1} 2^j
$$&lt;/p&gt;
&lt;p&gt;这意味着在任何时刻，保住更高位的 $1$ 永远比优化后面所有位产生的总和更具价值。&lt;/p&gt;
&lt;h4&gt;2、字典树：点对最大异或和&lt;/h4&gt;
&lt;p&gt;在处理 “两数异或” 问题时，字典树（01-Trie）是实现按位贪心策略的结构化利器。不同于线性基通过改变数值形态来压缩空间，01-Trie 完整地保留了原始数据的位信息，并将其映射到一棵深度固定的二叉树中。当我们需要在集合中找到一个数 $a_j$ 使得它与给定的 $a_i$ 异或结果最大时，其本质是在这棵代表数值空间的决策树上，进行一场基于高位优先原则的动态路径搜索。&lt;/p&gt;
&lt;p&gt;01-Trie 将集合中所有数的二进制表示看作从根节点出发的路径。从最高位（通常是第 $30$ 位或 $60$ 位）到最低位，左子树代表 $0$ ，右子树代表 $1$ 。这种结构将原本无序的数值集合转化为了一棵深度为 $\log(\max V)$ 的树，使得每一条从根到叶子的路径都唯一对应一个原始数值。&lt;/p&gt;
&lt;p&gt;在查询过程中，我们利用异或运算 “不同为 $1$ ” 的特性，对 $a_i$ 的二进制位从高到低进行扫描。对于 $a_i$ 的第 $k$ 位，如果该位的值为 $bit$ ，我们的核心贪心策略是：&lt;strong&gt;只要条件允许，永远优先向 $!bit$ 的分支移动&lt;/strong&gt; 。这种决策具有即时性和不可逆性：如果在当前层存在与 $a_i$ 某位相反的路径，我们必须选择该路径。因为即使这条路径会导致后续低位全部被迫选择相同位（异或结果为 $0$ ），高位贡献的 $1$ 依然在数值上绝对优于低位所有贡献的总和。&lt;/p&gt;
&lt;p&gt;假设当前处理到第 $k$ 位，我们期望寻找的分支是 $a_i \gg k \ &amp;amp; \ 1$ 的取反值。如果该分支存在，异或结果在这一位将产生确定的贡献：&lt;/p&gt;
&lt;p&gt;$$
contribution = 1 \ll k
$$&lt;/p&gt;
&lt;p&gt;此时，查询指针移动至对立分支，并继续向下递归。若该分支不存在，我们则被迫走向与 $a_i$ 当前位相同的分支，此时该位的异或贡献为 $0$ 。通过这种从高到低的逐位锁定，我们最终能在 $O(\log \max V)$ 的时间内精准定位到那个能产出最大火花的配对数。&lt;/p&gt;
&lt;p&gt;这种模型在处理 “最大异或子数组” 问题时展现了卓越的转化能力。根据异或运算的自反性（ $x \oplus x = 0$ ），任意区间 $[L, R]$ 的异或和可以转化为两个前缀异或和的异或：&lt;/p&gt;
&lt;p&gt;$$
XorSum(L, R) = PreXor[R] \oplus PreXor[L-1]
$$&lt;/p&gt;
&lt;p&gt;通过这一性质，求 “子数组最大异或和” 的问题便等价于 “在所有的前缀异或和中找到一对异或值最大的点” 。我们将所有 $PreXor$ 依次插入 01-Trie，每插入一个新前缀，就在树中查询与其配对的最优历史前缀。这种方法将原本 $O(n^2)$ 的暴力枚举优化至 $O(n \log M)$ ，实现了时间复杂度与数值范围的完美平衡。&lt;/p&gt;
&lt;h2&gt;最大异或乘积&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-xor-product/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你三个整数 &lt;code&gt;a&lt;/code&gt; 、&lt;code&gt;b&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; ，要求你从所有满足 $0 \le x &amp;lt; 2^n$ 的整数 $x$ 中选择一个，使得表达式&lt;/p&gt;
&lt;p&gt;$$
(a \oplus x) \times (b \oplus x)
$$&lt;/p&gt;
&lt;p&gt;的值最大。其中按位异或运算 $\oplus$ 是对两个整数的二进制位逐位运算，当对应位不同时结果为 $1$ ，相同为 $0$ 。&lt;/p&gt;
&lt;p&gt;由于结果可能非常大，请你将最终的最大值对 $10^9+7$ 取余后返回。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$0 \leq a, b &amp;lt; 2^{50}$&lt;/li&gt;
&lt;li&gt;$0 \leq n \leq 50$&lt;/li&gt;
&lt;li&gt;所有输入值均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$a \quad b \quad n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的最大值 $(a \oplus x) \times (b \oplus x)$ 对 $10^9+7$ 取余后的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12 5 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;98
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 7 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;930
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 6 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;本题要求在 $0 \leq x &amp;lt; 2^n$ 的范围内选择一个整数 $x$ ，使得 $(a \oplus x)(b \oplus x)$ 取得最大值。由于 $x$ 仅影响最低 $n$ 位，因此问题天然适合采用逐位分析的方法。对于位运算题目而言，&lt;strong&gt;常见的处理方式是拆位分析&lt;/strong&gt; ，因为按位运算本质上是逐位独立定义的。当我们决定采用拆位思路时，首先应当思考是否可以通过贪心提前确定某些位的操作，从而将问题规模逐步缩减。这种 “先确认显然最优的局部决策” 的思考方式，往往能够显著简化整体结构。&lt;/p&gt;
&lt;p&gt;我们引入两个变量：&lt;/p&gt;
&lt;p&gt;$$
A = a \oplus x, \quad B = b \oplus x
$$&lt;/p&gt;
&lt;p&gt;问题等价于最大化：&lt;/p&gt;
&lt;p&gt;$$
A \times B
$$&lt;/p&gt;
&lt;p&gt;为了理解如何选择 $x$ ，我们从每一位出发分析 $a_i$ 与 $b_i$ 的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当某一位满足 $a_i = b_i = 1$ 时，如果令 $x_i = 1$ ，则该位在 $A$ 与 $B$ 中都会变为 $0$ ；若令 $x_i = 0$ ，则保持为 $1$ 。显然，同时减少两个 $1$ 会降低乘积，因此这一位的最优策略是保持不变，即令 $x_i = 0$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当某一位满足 $a_i = b_i = 0$ 时，如果令 $x_i = 1$ ，则该位在 $A$ 与 $B$ 中都会变为 $1$ ；若令 $x_i = 0$ ，则保持为 $0$ 。显然，同时增加两个 $1$ 会提升乘积，因此这一位的最优策略是保持不变，即令 $x_i = 1$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过上述两类情况的分析后，可以发现这些位的贡献已经完全确定，它们构成了一个不可改变的 “固定部分” 。真正影响优化结构的，是那些满足 $a_i \ne b_i$ 的位。在这些不同位上，若某位原本是 $a_i = 1, b_i = 0$ ，则当 $x_i = 0$ 时保持原状，当 $x_i = 1$ 时二者交换。因此，在这些位上，$x$ 的作用本质上是决定该位权值分配给 $A$ 还是 $B$ 。需要注意的是，无论如何分配，该位的权值 $2^i$ 总是存在于两者之一，因此这些位上的总权值是固定的，只是可以在两数之间重新分配。&lt;/p&gt;
&lt;p&gt;设固定部分分别为 $C_A$ 与 $C_B$ ，所有不同位的权值总和为 $T$ 。则可以写成：&lt;/p&gt;
&lt;p&gt;$$
A = C_A + x
$$&lt;/p&gt;
&lt;p&gt;$$
B = C_B + (T - x)
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 表示分配给 $A$ 的那部分权值。于是乘积为：&lt;/p&gt;
&lt;p&gt;$$
(C_A + x)(C_B + T - x)
$$&lt;/p&gt;
&lt;p&gt;这是一个关于 $x$ 的二次函数。将其展开可以得到负的二次项系数，因此函数开口向下，存在唯一最大值。根据二次函数的基本性质可知，在总资源 $T$ 固定的情况下，当两数尽可能接近时，乘积达到最大。因此，最优策略是在可交换位上尽量平衡两数的大小。由于二进制数中高位权重大于低位，在具体实现时应当从高位到低位进行贪心决策，每当遇到一个可以分配的位，就优先将其分配给当前数值较小的一方，从而保持两数尽可能接近。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组末尾最小值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-array-end/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你两个整数 &lt;code&gt;n&lt;/code&gt; 和 &lt;code&gt;x&lt;/code&gt; 。你需要构造一个长度为 $n$ 的正整数数组 &lt;code&gt;nums&lt;/code&gt; ，满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对所有 $0 \leq i &amp;lt; n - 1$ ，都有 &lt;code&gt;nums[i + 1] &amp;gt; nums[i]&lt;/code&gt; ；&lt;/li&gt;
&lt;li&gt;数组 &lt;code&gt;nums&lt;/code&gt; 中所有元素的按位 AND 运算结果等于 $x$ ；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请返回数组 &lt;code&gt;nums&lt;/code&gt; 的最后一个元素 &lt;code&gt;nums[n - 1]&lt;/code&gt; 的 &lt;strong&gt;最小可能值&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n, x \leq 10^8$&lt;/li&gt;
&lt;li&gt;输出的最后一个元素必为正整数&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad x$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的数组 &lt;code&gt;nums&lt;/code&gt; 的最小可能的最后一个元素 &lt;code&gt;nums[n - 1]&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;本题依旧是一道典型的位运算构造问题，因此自然应当从拆位的角度进行分析。对于位运算题目，我们通常优先考虑逐位拆解，并结合贪心思想，尽可能先确定那些可以直接确定的 “固定贡献” ，从而将问题转化为剩余部分的结构优化。&lt;/p&gt;
&lt;p&gt;题目要求构造一个严格递增的正整数数组 &lt;code&gt;nums&lt;/code&gt; ，并满足所有元素的按位与结果等于 $x$ 。设数组为：&lt;/p&gt;
&lt;p&gt;$$
nums_0, , nums_1, , \dots, , nums_{n-1}
$$&lt;/p&gt;
&lt;p&gt;并要求：&lt;/p&gt;
&lt;p&gt;$$
nums_0 , &amp;amp; , nums_1 , &amp;amp; , \cdots , &amp;amp; , nums_{n-1} = x
$$&lt;/p&gt;
&lt;p&gt;按位与运算的性质决定了：某一位最终为 $1$ ，当且仅当 &lt;strong&gt;所有数在该位上都为 $1$&lt;/strong&gt; 。因此，如果 $x$ 的某一位是 $1$ ，那么数组中每一个元素在这一位上都必须是 $1$ 。这些位是完全被强制确定的，不存在任何选择空间。接下来考虑 $x$ 中为 $0$ 的位。对于这些位，只需保证在所有元素中至少有一个数在该位上为 $0$ ，即可保证最终按位与结果为 $0$ 。但由于我们希望数组严格递增且最后一个数尽量小，因此最优策略是尽量以最小幅度递增构造数组。&lt;/p&gt;
&lt;p&gt;关键观察在于，既然所有数都必须包含 $x$ 的 $1$ 位，那么数组中的每个元素都可以写成：&lt;/p&gt;
&lt;p&gt;$$
nums_i = x , | , y_i
$$&lt;/p&gt;
&lt;p&gt;其中 $y_i$ 只在 $x$ 为 $0$ 的位上可以自由取值。于是问题转化为：构造一个严格递增的序列 $y_0 &amp;lt; y_1 &amp;lt; \cdots &amp;lt; y_{n-1}$ ，并使得最终的 $nums_{n-1}$ 最小。&lt;/p&gt;
&lt;p&gt;为了让 $nums_{n-1}$ 尽可能小，就应当让 $y_i$ 本身尽可能小且按最小步长递增。显然，最优构造方式是令：&lt;/p&gt;
&lt;p&gt;$$
y_i = i
$$&lt;/p&gt;
&lt;p&gt;但需要注意的是，这里的 $i$ 不是直接填入原数，而是将 $i$ 的二进制表示嵌入到 $x$ 为 $0$ 的那些位中。也就是说，我们将自然数序列 $0, 1, 2, 3, \dots, n-1$ 的二进制表示依次填入 $x$ 为 $0$ 的位置上，从低位到高位对应填充。&lt;/p&gt;
&lt;p&gt;这样构造得到的序列天然严格递增，同时保证所有元素都包含 $x$ 的固定 $1$ 位，因此按位与结果恰好为 $x$ 。由于我们使用的是从 $0$ 开始的最小连续整数序列，因此最终得到的 $nums_{n-1}$ 也是所有合法构造中的最小值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;平方和最大操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/apply-operations-on-array-to-maximize-sum-of-squares/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个 0-索引的整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个正整数 &lt;code&gt;k&lt;/code&gt;。你可以对数组执行如下操作任意次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从数组中任选两个不同的下标 &lt;code&gt;i&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt;，同时将&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nums[i]&lt;/code&gt; 更新为 &lt;code&gt;(nums[i] AND nums[j])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums[j]&lt;/code&gt; 更新为 &lt;code&gt;(nums[i] OR nums[j])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 &lt;code&gt;AND&lt;/code&gt; 和 &lt;code&gt;OR&lt;/code&gt; 分别表示 &lt;strong&gt;按位与&lt;/strong&gt; 和 &lt;strong&gt;按位或&lt;/strong&gt; 操作。&lt;/p&gt;
&lt;p&gt;你可以在执行若干次（或不执行）操作之后，从最终的数组中选出 &lt;strong&gt;k 个元素&lt;/strong&gt; 并计算它们的平方和。请你返回最大可能的平方和，由于结果可能很大，返回值对 $10^9 + 7$ 取余。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq k \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，其中 $n$ 表示数组长度，$k$ 表示选取个数。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的每个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在执行任意合法操作后，选出 $k$ 个元素的最大平方和，并对 $10^9+7$ 取余。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 2
2 6 5 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;261
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
4 5 4 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;90
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题依然是一类典型的位运算重排问题，因此自然应当从拆位分析入手。对于涉及 AND、OR 之类按位操作的题目，我们通常优先逐位研究其变化规律，寻找是否存在守恒量或固定贡献。一旦发现某种 “不可改变的结构” ，问题往往就会从复杂的操作模拟，转化为更本质的资源分配问题。&lt;/p&gt;
&lt;p&gt;题目给定的操作为：任选两个数 $a, b$ ，执行：&lt;/p&gt;
&lt;p&gt;$$
a \leftarrow a , &amp;amp; , b, \quad b \leftarrow a , | , b
$$&lt;/p&gt;
&lt;p&gt;为了理解其本质，我们只需考察单个位上的变化情况。设某一位上两数分别为 $(x,y)$ 。如果是 $(0,0)$ 或 $(1,1)$ ，显然操作后不发生变化；如果是 $(0,1)$ ，按位与得到 $0$ ，按位或得到 $1$ ，结果仍为 $(0,1)$ ；只有在 $(1,0)$ 时，会被调整为 $(0,1)$ 。&lt;/p&gt;
&lt;p&gt;从这四种情况可以看出一个核心事实：每一位上的 $1$ 的总数量保持不变。操作只是在不同元素之间 “转移” 该位上的 $1$ ，而不会创造或消灭 $1$ 。因此，设第 $b$ 位上最初共有 $cnt_b$个 $1$ ，那么无论如何操作，最终这一位上仍然有且仅有 $cnt_b$ 个 $1$ 。这一步实际上已经揭示了问题的本质：我们可以在数组元素之间自由重新分配每一位上的 $1$ ，但各个位的 $1$ 的总数是守恒的。于是原问题等价于——在每一位上拥有 $cnt_b$ 个权值为 $2^b$ 的资源，可以任意分配到各个元素中。&lt;/p&gt;
&lt;p&gt;接下来考虑目标函数。我们最终要从数组中选出 $k$ 个元素，使得它们的平方和最大：&lt;/p&gt;
&lt;p&gt;$$
\max \sum_{i=1}^{k} a_i^2
$$&lt;/p&gt;
&lt;p&gt;平方函数是严格凸函数，这意味着在总和一定的情况下，将数值集中在少数元素上，比平均分配能获得更大的平方和，也就是：&lt;/p&gt;
&lt;p&gt;$$
(x+y)^2 &amp;gt; x^2 + y^2 \quad (\text{if } x,y&amp;gt;0)
$$&lt;/p&gt;
&lt;p&gt;因此，既然每一位上的贡献可以自由分配，那么最优策略必然是尽可能把各个位上的 $1$ 叠加到同一批元素上，从而构造出尽可能大的数。具体实现时，可以按如下贪心思路进行：统计所有 $cnt_b$ ，然后重复 $k$ 轮构造。每一轮构造一个新的数，对于所有满足 $cnt_b &amp;gt; 0$ 的位，将对应的 $2^b$ 加入当前数，并令：&lt;/p&gt;
&lt;p&gt;$$
cnt_b \leftarrow cnt_b - 1
$$&lt;/p&gt;
&lt;p&gt;这样得到的第一个数最大，第二个数次大，以此类推。由于每一轮都尽可能收集所有剩余的高位贡献，数值会呈现逐层递减的结构，而平方和则因高度集中而被最大化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最大异或和操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-xor-after-operations/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个 &lt;strong&gt;0-索引&lt;/strong&gt; 的整数数组 &lt;code&gt;nums&lt;/code&gt; 。在一次操作中，你可以选择任意一个 &lt;strong&gt;非负整数&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt; 和一个下标 &lt;code&gt;i&lt;/code&gt; ，然后将 &lt;code&gt;nums[i]&lt;/code&gt; 更新为：&lt;/p&gt;
&lt;p&gt;$$
nums[i] = nums[i] , &amp;amp; , (nums[i] \oplus x)
$$&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;AND&lt;/code&gt; 是按位与运算，&lt;code&gt;XOR&lt;/code&gt; 是按位异或运算。&lt;/p&gt;
&lt;p&gt;请返回在执行任意次数（包括 0 次）操作后，数组中所有元素按位异或（XOR）之后能获得的 &lt;strong&gt;最大可能值&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^8$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在经过任意次数操作后，数组所有元素按位 XOR 的最大可能值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2 4 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2 3 9 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的关键在于先理解给定操作到底能做什么。题目允许我们进行如下操作：&lt;/p&gt;
&lt;p&gt;$$
nums[i] = nums[i] , &amp;amp; , (nums[i] \oplus x)
$$&lt;/p&gt;
&lt;p&gt;注意到最外层是一个按位与运算，左边的 &lt;code&gt;nums[i]&lt;/code&gt; 本身就相当于一个 “掩码” 。这意味着，无论我们怎样选择 $x$ ，运算结果都不可能在原本为 $0$ 的位上变成 $1$ 。换句话说，这个操作只能把某些 $1$ 变成 $0$ ，而不能凭空增加新的 $1$ 。再看中间的异或部分。由于 $x$ 可以是任意非负整数，因此 &lt;code&gt;nums[i] ^ x&lt;/code&gt; 可以在每一位上自由控制翻转与否。结合外层的按位与，可以发现我们实际上可以 “选择性地关闭” &lt;code&gt;nums[i]&lt;/code&gt; 中的任意若干个 $1$ 。也就是说，经过若干次操作之后，&lt;code&gt;nums[i]&lt;/code&gt; 可以变成它的任意一个子集，但绝对不能增加新的 $1$ 。&lt;/p&gt;
&lt;p&gt;因此，本题的本质就变成了：我们可以对每个元素，删除它的一些 $1$ 位，但不能新增 $1$ 位。最终目标是让整个数组的异或值最大。&lt;/p&gt;
&lt;p&gt;接下来从异或的角度分析。一个整数的异或结果在某一位上为 $1$ ，当且仅当该位上所有元素中 $1$ 的个数为奇数。既然我们可以删除某些 $1$ ，那么在每一位上，我们唯一能做的事情就是减少 $1$ 的数量。为了让最终异或结果最大，我们希望在尽可能高的位上得到 $1$ 。而某一位能否最终为 $1$ ，取决于这一位在所有元素中是否 “至少出现过一次” 。如果某一位在原数组中从未出现过 $1$ ，那么由于我们不能增加 $1$ ，这一位最终必然为 $0$ 。&lt;/p&gt;
&lt;p&gt;而如果某一位原本出现过至少一次 $1$ ，那么我们就可以通过删除多余的 $1$ ，使这一位的 $1$ 的总数变成奇数，从而保证最终异或结果在这一位为 $1$ 。因为只要这一位原本有至少一个 $1$ ，我们就可以保留一个、删除其余全部，使其数量为 $1$（奇数）。因此，对于每一位，只要原数组中存在至少一个元素在该位为 $1$ ，我们就一定可以让最终异或结果在这一位为 $1$ ；如果原本没有，则永远无法得到 $1$ 。&lt;/p&gt;
&lt;p&gt;这说明最终答案恰好等于所有元素的按位或结果：&lt;/p&gt;
&lt;p&gt;$$
nums[0] , | , nums[1] , | , \cdots , | , nums[n-1]
$$&lt;/p&gt;
&lt;p&gt;因为按位或正好表示 “每一位是否至少出现过一次 1” 。&lt;/p&gt;
&lt;p&gt;综上所述，这道题虽然操作形式复杂，但本质上只是一个位运算性质的转化问题。操作只能删除 $1$ ，不能增加 $1$ ；而异或最大化只关心每一位是否能保留奇数个 $1$ 。最终答案就是原数组的按位或值，时间复杂度为 $O(n)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最短or路径问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc408/tasks/abc408_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个由 $1$ 到 $N$ 编号的无向连通图，共有 $N$ 个顶点和 $M$ 条边。图中不包含自环，但可能包含重边。第 $i$ 条边连接顶点 $u_i$ 和顶点 $v_i$ ，并带有一个非负整数权值 $w_i$ 。&lt;/p&gt;
&lt;p&gt;对于任意一条从顶点 $1$ 到顶点 $N$ 的 &lt;strong&gt;简单路径&lt;/strong&gt;（不重复经过同一顶点），定义该路径的代价为路径上所有边权的按位 OR 运算结果，即：&lt;/p&gt;
&lt;p&gt;$$
w_{i_1} , \mathrm{OR}\ w_{i_2} , \mathrm{OR} , \cdots , \mathrm{OR} , w_{i_k}
$$&lt;/p&gt;
&lt;p&gt;其中每个 $w_{i_j}$ 是路径所包含的边的权值。&lt;/p&gt;
&lt;p&gt;请找出所有从顶点 $1$ 到顶点 $N$ 的简单路径中，使上述 OR 结果最小的那个值，并输出该最小值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$N - 1 \leq M \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq u_i &amp;lt; v_i \leq N$&lt;/li&gt;
&lt;li&gt;$0 \leq w_i &amp;lt; 2^{30}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入格式如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1 \quad w_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2 \quad w_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_M \quad v_M \quad w_M$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示从顶点 $1$ 到顶点 $N$ 的所有简单路径中，按位 OR 运算结果的最小可能值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 5
1 2 1
1 3 4
2 3 2
2 4 4
3 4 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 5
1 2 1
1 2 2
1 2 3
1 2 4
2 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8 12
4 5 16691344
5 7 129642441
2 7 789275447
3 8 335307651
3 5 530163333
5 6 811293773
3 8 333712701
1 2 2909941
2 3 160265478
5 7 465414272
1 3 903373004
6 7 408299562
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;468549631
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;或位运算&lt;/p&gt;
&lt;h2&gt;异或数列博弈论&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8743&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;异或位运算&lt;/p&gt;
&lt;h2&gt;最大异或对构造&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF1285D&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;异或位运算&lt;/p&gt;
&lt;h2&gt;位运算和式变换&lt;/h2&gt;
&lt;p&gt;在一些涉及 &lt;strong&gt;位运算的求和问题&lt;/strong&gt; 中，直接按照题意枚举所有元素往往是不可行的，因为数据范围通常非常大。这时，一个非常常见的技巧是对表达式进行 &lt;strong&gt;和式变换（sum transformation）&lt;/strong&gt;：将原本关于整数的运算，转化为 &lt;strong&gt;按位统计贡献&lt;/strong&gt; 的形式。&lt;/p&gt;
&lt;p&gt;其核心思想来源于位运算的 &lt;strong&gt;按位独立性&lt;/strong&gt; 。例如在计算大量异或、与、或运算的和时，可以将问题拆解为：&lt;strong&gt;每一位对最终答案的贡献&lt;/strong&gt; 。对于某一位 $k$ ，只需要统计在所有数中这一位为 $0$ 或 $1$ 的数量，然后计算能够使该位为 $1$ 的组合数量，再乘上对应的权值 $2^k$ 。这样就把一个复杂的双重求和问题，转化为对每一位进行一次简单计数。&lt;/p&gt;
&lt;p&gt;例如在求解下面这个累加和：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n}\sum_{j=1}^{m}(i\oplus j)
$$&lt;/p&gt;
&lt;p&gt;我们可以对每一位 $k$ 单独计算贡献。如果 $1 \sim n$ 中该位为 $1$ 的数量为 $a$ ，为 $0$ 的数量为 $n-a$ ，而 $1 \sim m$ 中该位为 $1$ 的数量为 $b$ ，为 $0$ 的数量为 $m-b$ ，那么只有 &lt;strong&gt;0 与 1 配对&lt;/strong&gt; 才会使该位异或结果为 $1$ ，因此该位的贡献数量为：&lt;/p&gt;
&lt;p&gt;$$
\Big[a(m - b) + b(n - a)\Big] \times 2^k
$$&lt;/p&gt;
&lt;p&gt;通过这种方式，原本需要枚举 $nm$ 个配对的问题，可以被转化为 &lt;strong&gt;按位统计 + 数学计数&lt;/strong&gt; 的过程，复杂度通常只需要 $O(\log n)$ 或 $O(\log m)$ 。这种思路在许多位运算求和问题中都非常常见，是逐位拆解方法的重要应用场景。从更深层次来看，这种方法之所以成立，尤其在异或问题中表现突出，是因为 &lt;strong&gt;异或运算本质上等价于二进制下的模 2 加法（无进位加法）&lt;/strong&gt;。在每一位上，其运算规则为：&lt;/p&gt;
&lt;p&gt;$$
0 \oplus 0 = 0,\quad
0 \oplus 1 = 1,\quad
1 \oplus 0 = 1,\quad
1 \oplus 1 = 0
$$&lt;/p&gt;
&lt;p&gt;因此，可以把整数看作由 $0/1$ 组成的二进制向量，而异或运算就是对这些向量进行 &lt;strong&gt;逐位的模 2 加法&lt;/strong&gt; 。由于不存在进位，不同二进制位之间完全独立，这也使得 “按位统计贡献” 的和式变换方法在异或问题中具有天然优势。&lt;/p&gt;
&lt;p&gt;在其他类似的问题中，我们还可以将 “寻找满足某种异或关系的数对” 转化为寻找对应的 &lt;strong&gt;目标值&lt;/strong&gt; 。这种结构与经典的&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/two-sum-idea/two-sum-idea/&quot;&gt;两数之和思想&lt;/a&gt;非常相似：在遍历数组时，可以用哈希表记录已经出现过的元素，并查询当前元素对应的目标值 $a_i \oplus x$ 是否存在。如果存在，我们就找到了一组满足条件的数对。&lt;/p&gt;
&lt;h2&gt;数组异或序列和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3917&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;子段和的异或和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3760&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;BIT性质分析问题&lt;/h1&gt;
&lt;p&gt;除了常见的逐位拆解思路之外，还有一类位运算问题并不适合直接按位分析，而是需要通过挖掘运算本身的 &lt;strong&gt;特殊性质&lt;/strong&gt; 来解决。这类题目的关键往往不在于逐位枚举，而在于观察某些操作在整体结构上的变化规律。一旦发现这些规律，问题往往可以被大幅简化，甚至转化为一个完全不同类型的模型。&lt;/p&gt;
&lt;p&gt;因此，在处理位运算问题时，除了考虑逐位分析之外，也可以尝试从 &lt;strong&gt;运算性质&lt;/strong&gt; 的角度进行思考。例如某些运算具有单调性、可逆性或抵消性质，这些结构特征往往会对状态变化产生强约束。通过利用这些性质，可以快速排除大量不可能的情况，从而显著降低问题的复杂度。&lt;/p&gt;
&lt;h2&gt;AOR相关性质&lt;/h2&gt;
&lt;p&gt;按位与和按位或运算都具有非常明显的 &lt;strong&gt;单调结构&lt;/strong&gt; 。按位与运算会使数值 &lt;strong&gt;单调不增&lt;/strong&gt;：某一位一旦在某次运算中变为 $0$ ，之后再进行与运算时就不可能恢复为 $1$ 。因此，在只包含按位与操作的过程中，整体数值只会不断减小，很多较大的状态从一开始就不可能被达到。这一性质在区间问题、动态维护以及状态压缩中经常被利用，例如区间按位与的不同结果数量通常是非常有限的。&lt;/p&gt;
&lt;p&gt;与之相对，按位或运算则具有 &lt;strong&gt;单调不减&lt;/strong&gt; 的特征：某一位一旦变为 $1$ ，之后通常无法再恢复为 $0$ 。因此，在只包含按位或操作的过程中，数值会不断增大，状态变化也会逐渐趋于稳定。利用这一性质，可以快速判断某些状态是否可达，并且在许多问题中可以证明不同结果的数量是受限的，从而避免枚举所有可能的状态。&lt;/p&gt;
&lt;p&gt;此外，按位与和按位或还具有 &lt;strong&gt;幂等性&lt;/strong&gt; ，即：&lt;/p&gt;
&lt;p&gt;$$
a , &amp;amp; , a = a \quad a , | , a = a
$$&lt;/p&gt;
&lt;p&gt;这意味着重复执行同样的操作不会产生新的结果。在某些操作序列问题中，这种性质会导致过程在有限步之后 &lt;strong&gt;收敛到稳定状态&lt;/strong&gt; ，从而使得原本需要大量模拟的过程可以被显著简化。&lt;/p&gt;
&lt;h2&gt;多数目子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/split-array-into-maximum-number-of-subarrays/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个由非负整数组成的数组 &lt;code&gt;nums&lt;/code&gt; 。我们定义子数组 &lt;code&gt;nums[l..r]&lt;/code&gt; 的 &lt;strong&gt;得分&lt;/strong&gt; 为：&lt;/p&gt;
&lt;p&gt;$$
nums[l] , &amp;amp; , nums[l+1] , &amp;amp; , \cdots , &amp;amp; , nums[r]
$$&lt;/p&gt;
&lt;p&gt;其中 $&amp;amp;$ 表示按位 与 操作。&lt;/p&gt;
&lt;p&gt;现在你需要将数组分割成若干个 &lt;strong&gt;连续子数组&lt;/strong&gt; ，满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组中的每个元素恰好属于一个子数组；&lt;/li&gt;
&lt;li&gt;所有子数组得分之和 &lt;strong&gt;最小&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在满足上述条件的前提下，返回能构造出的子数组 &lt;strong&gt;最多个数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这里的子数组必须是连续的数组片段。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^6$&lt;/li&gt;
&lt;li&gt;数组 &lt;code&gt;nums&lt;/code&gt; 中的所有元素均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $b$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在满足最小得分和的前提下，能够划分出的最大子数组数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 0 2 0 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 7 1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的核心在于理解按位与运算的单调性质。对于一段区间而言，随着参与按位与的元素个数增加，结果只会保持不变或变小，而不可能变大。换句话说，区间越长，其按位与的结果越不利于增大总和。因此，在考虑如何划分子数组时，应当先分析整个数组的整体按位与结果。&lt;/p&gt;
&lt;p&gt;设整个数组的按位与为：&lt;/p&gt;
&lt;p&gt;$$
T = nums_0 , &amp;amp; , nums_1 , &amp;amp; , \cdots , &amp;amp; , nums_{n-1}
$$&lt;/p&gt;
&lt;p&gt;显然，任意子数组的按位与结果都不会小于 $T$ 。因为整体按位与已经包含了所有元素的约束，是能够达到的最小值。也就是说，所有合法划分的子数组得分之和，至少为若干个不小于 $T$ 的数之和。&lt;/p&gt;
&lt;p&gt;如果 $T &amp;gt; 0$ ，那么问题立刻变得简单。因为无论如何划分，每个子数组的按位与结果都不小于 $T$ ，一旦拆分成多个子数组，得分之和就至少为：&lt;/p&gt;
&lt;p&gt;$$
T + T + \cdots
$$&lt;/p&gt;
&lt;p&gt;显然会严格大于单个整体区间的得分 $T$ 。既然题目要求总得分最小，那么此时最优策略只能是不拆分数组，即整个数组作为一个子数组，答案为 $1$ 。&lt;/p&gt;
&lt;p&gt;接下来考虑 $T = 0$ 的情况。既然整体按位与已经为 $0$ ，说明存在某些位在不同元素之间互相 “抵消” ，最终使得整段区间的按位与结果为零。由于按位与运算是单调递减的，我们希望尽可能多地构造出按位与为 $0$ 的子数组。因为一旦某个子数组的得分为 $0$ ，它对总和的贡献就是最小的。&lt;/p&gt;
&lt;p&gt;因此，在 $T = 0$ 的情况下，我们的目标转化为：将数组划分为尽可能多的连续区间，使得每个区间的按位与结果为 $0$ 。这样总得分仍然为 $0$ ，同时子数组数量最多。具体实现时，可以从左到右维护一个当前区间的按位与值，初始设为一个全 $1$ 的掩码或直接从第一个元素开始累积。每次将当前元素与进来，当结果变为 $0$ 时，就立即在此处切断，计数加一，并重新开始下一段区间的累积。由于按位与只会变小，一旦为 $0$ 就不可能再恢复，因此此时切分是最优且贪心正确的。&lt;/p&gt;
&lt;p&gt;综上，本题的关键在于先计算整体按位与，分情况讨论。当整体按位与大于 $0$ 时答案为 $1$ ；当整体按位与为 $0$ 时，通过贪心切分每一个前缀按位与首次变为 $0$ 的位置，可以得到最多的子数组数量。这种思路的本质是利用按位与的单调性，将问题转化为 “尽可能多地制造零贡献区间” 的结构化贪心问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;按位或最大数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/smallest-subarrays-with-maximum-bitwise-or/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个长度为 $n$ 的 &lt;strong&gt;0-索引&lt;/strong&gt; 数组 &lt;code&gt;nums&lt;/code&gt; ，数组由非负整数组成。对于每个下标 $i$ （ $0 \leq i &amp;lt; n$ ），你需要找出从位置 $i$ 开始的 &lt;strong&gt;最短非空子数组&lt;/strong&gt; ，使得这个子数组的 &lt;strong&gt;按位 OR 运算值&lt;/strong&gt; 是从位置 $i$ 到末尾所有可能子数组中所能获得的 &lt;strong&gt;最大 OR 值&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;换句话说，记 $B_{i,j}$ 为子数组 &lt;code&gt;nums[i..j]&lt;/code&gt; 的按位 OR 值，你要找出最小的 $j$ 满足：&lt;/p&gt;
&lt;p&gt;$$
nums[i] , \text{OR} , nums[i{+}1] , \text{OR} , \cdots , \text{OR} , nums[j] = \max_{i \leq k &amp;lt; n} \left( nums[i] , \text{OR} , \cdots , \text{OR} , nums[k] \right)
$$&lt;/p&gt;
&lt;p&gt;按位 OR 运算定义为：对二进制数的每一位进行 OR，如果任一输入在该位是 $1$ ，则输出为 $1$ 。&lt;/p&gt;
&lt;p&gt;请你返回一个整数数组 &lt;code&gt;answer&lt;/code&gt; ，长度为 $n$ ，其中 &lt;code&gt;answer[i]&lt;/code&gt; 是从位置 $i$ 开始，取得最大按位 OR 值所需的最短子数组长度。子数组必须是连续的非空片段。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == nums.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $b$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行整数数组 &lt;code&gt;answer&lt;/code&gt; ，其中 &lt;code&gt;answer[i]&lt;/code&gt; 表示从下标 $i$ 开始的最短子数组长度，该子数组的按位 OR 值等于从 $i$ 出发能达到的最大按位 OR 值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 0 2 1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3 2 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题如果换成 “子数组最大累加和” ，我们往往只需要维护某种前缀或后缀的最优结构即可，因为加法具有可逆性和线性结构。但按位或运算并不具备这种性质，它是不可逆的，因此必须从按位或本身的结构特征入手分析。&lt;/p&gt;
&lt;p&gt;首先注意按位或的单调性，设：&lt;/p&gt;
&lt;p&gt;$$
A_{i,j} = nums[i] , | , nums[i+1] , | , \cdots , | , nums[j]
$$&lt;/p&gt;
&lt;p&gt;当我们固定 $i$ ，不断增大 $j$ 时，$A_{i,j}$ 只可能变大或保持不变，不可能变小。因为某一位一旦在或运算中被置为 $1$ ，之后就不可能再变回 $0$ 。因此对于每个起点 $i$ ，所谓 “最大按位或值” ，实际上就是扩展到数组末尾得到的值：&lt;/p&gt;
&lt;p&gt;$$
A_{i,n-1}
$$&lt;/p&gt;
&lt;p&gt;问题就转化为：对于每个 $i$ ，找到最小的 $j$ ，使得：&lt;/p&gt;
&lt;p&gt;$$
A_{i,j} = A_{i,n-1}
$$&lt;/p&gt;
&lt;p&gt;如果从每个 $i$ 暴力向右扩展，时间复杂度会达到 $O(n^2)$ 。但按位或还有一个更关键的性质：由于整数最多只有 $32$ 位，而每一位最多只会从 $0$ 变成 $1$ 一次，因此对于固定起点 $i$ ，随着 $j$ 的增加，$A_{i,j}$ 的不同取值不超过 $32$ 种。&lt;/p&gt;
&lt;p&gt;换一个角度来看，我们从右向左动态维护 “所有后缀按位或结果” 。设当前处理到位置 $i$ ，我们知道从 $i+1$ 开始的所有不同的后缀或值。将 $nums[i]$ 与这些值分别进行一次或运算，再加上单独的 $nums[i]$ ，就得到了从 $i$ 开始的所有后缀或值。&lt;/p&gt;
&lt;p&gt;这里有一个非常重要的细节：后缀按位或值在扩展过程中是单调不减的，因此生成的新或值序列也是单调的。由于单调性，相同的或值一定是连续出现的。换句话说，若在更新过程中产生了重复值，那么这些重复值必然是相邻的，可以在构造时直接去重。&lt;/p&gt;
&lt;p&gt;因此在实现时，我们只需维护一个有序数组（或列表）表示不同的后缀或值。每次更新时按顺序计算新的或值序列，并在生成过程中去掉与前一个相同的值即可。由于总状态数不超过 $32$ 个，因此每一步的复杂度是常数级的。&lt;/p&gt;
&lt;p&gt;在得到从 $i$ 开始的所有不同后缀或值后，最大值就是该列表中的最后一个元素。由于我们在维护时可以同时记录每个或值最远延伸到的下标，因此可以直接得到最短的 $j$ ，从而计算答案长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目相关拓展&lt;/h2&gt;
&lt;p&gt;如果把本题中的 “最大 OR 值” 改成与 “最大 XOR 值” ，那么问题结构会发生明显变化。设我们定义后缀异或和：&lt;/p&gt;
&lt;p&gt;$$
S_i = nums[i] \oplus nums[i+1] \oplus \cdots \oplus nums[n-1]
$$&lt;/p&gt;
&lt;p&gt;则任意区间 $[i, j]$ 的异或值可以表示为：&lt;/p&gt;
&lt;p&gt;$$
nums[i] \oplus \cdots \oplus nums[j] = S_i \oplus S_{j+1}
$$&lt;/p&gt;
&lt;p&gt;因此，如果我们希望在固定起点 $i$ 的情况下，使某个后缀子区间的异或值最大，本质上就是在所有可能的 $S_{j+1}$ 中，找到一个值，使得 $S_i \oplus S_{j+1}$ 最大。这就转化成了一个经典问题：&lt;strong&gt;给定一个数，在一组数中找到与它异或结果最大的数&lt;/strong&gt; 。这正是 “最大异或对” 问题。&lt;/p&gt;
&lt;p&gt;解决这类问题的标准做法是使用二进制字典树（Trie）。具体思路如下：&lt;/p&gt;
&lt;p&gt;我们从右向左遍历数组，动态计算后缀异或和 $S_i$ 。在处理到位置 $i$ 时，Trie 中已经存储了所有 $S_{j}$（其中 $j &amp;gt; i$ ）。此时我们在 Trie 中查询一个数，使得与当前 $S_i$ 异或的结果最大。Trie 的构造方式是按二进制位从高位到低位插入。查询时，为了让异或结果最大，我们在每一位上尽量选择与当前位相反的分支（若存在）。这样贪心地从高位优先保证结果尽可能大，最终得到全局最优解。&lt;/p&gt;
&lt;p&gt;由于整数最多 $32$ 位，每次插入和查询的复杂度都是 $O(32)$ ，整体时间复杂度为 $O(32n)$ ，可以视为线性。&lt;/p&gt;
&lt;h2&gt;最长优雅子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-nice-subarray/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个由正整数组成的数组 &lt;code&gt;nums&lt;/code&gt; 。当一个子数组中任意两个 &lt;strong&gt;不同位置&lt;/strong&gt; 的元素按位与（AND）运算结果为 &lt;code&gt;0&lt;/code&gt; 时，我们称这个子数组为 &lt;strong&gt;优雅子数组&lt;/strong&gt;（nice subarray）。按位与运算定义为：对于两个整数，对各个二进制位进行 AND 操作，当对应位都为 &lt;code&gt;1&lt;/code&gt; 时该位结果为 &lt;code&gt;1&lt;/code&gt; ，否则为 &lt;code&gt;0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;请你返回数组 &lt;code&gt;nums&lt;/code&gt; 中 &lt;strong&gt;最长的优雅子数组的长度&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;子数组指数组中的一个连续部分。注意，&lt;strong&gt;长度为 1 的子数组总是优雅的&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示数组中最长的优雅子数组的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 3 8 48 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 1 5 11 13
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;题目要求子数组中任意两个不同位置的元素按位与结果为 $0$ 。等价地说，任意两个数在二进制表示中不能在同一位上同时为 $1$ 。也就是说，这个子数组中的所有数字，在二进制层面上 $1$ 的位置必须两两互不重叠。&lt;/p&gt;
&lt;p&gt;从这个条件可以立刻得到一个重要结论：一个优雅子数组的长度不会超过二进制位数上限。因为一个整数最多只有 $32$ 位（在本题数据范围内甚至不到 $32$ 位），而每一位最多只能被一个元素占用。如果两个元素在同一位上都有 $1$ ，那么它们的按位与一定不为 $0$ ，子数组就不合法。因此，一个优雅子数组的最大长度最多是 $32$ ，也就是 “每个数只贡献一个不同的 $1$ 位” 的极端情况。&lt;/p&gt;
&lt;p&gt;这个上界非常关键。数组长度可以达到 $10^5$ ，但真正需要考虑的窗口长度最多只有 $32$ 。也就是说，对于每个起点，向右最多扩展 $32$ 步就可以确定该起点能形成的最长优雅子数组。最直接的做法是枚举左端点 $i$ ，然后从 $i$ 开始向右扩展窗口，维护当前窗口内所有元素的按位或值 &lt;code&gt;mask&lt;/code&gt; 。当我们尝试加入一个新元素 &lt;code&gt;nums[j]&lt;/code&gt; 时，只需要检查：&lt;/p&gt;
&lt;p&gt;$$
mask , &amp;amp; , nums[j] == 0
$$&lt;/p&gt;
&lt;p&gt;如果成立，说明没有任何位冲突，可以加入窗口，并更新：&lt;/p&gt;
&lt;p&gt;$$
mask = mask , | , nums[j]
$$&lt;/p&gt;
&lt;p&gt;一旦不成立，就停止扩展当前起点。由于窗口长度最多 $32$ ，每个起点最多检查 $32$ 次，总时间复杂度为 $O(32n)$ ，可以视为线性复杂度。&lt;/p&gt;
&lt;p&gt;当然，也可以用更标准的滑动窗口写法。用双指针维护一个区间 &lt;code&gt;[l, r)&lt;/code&gt; ，同时维护当前窗口的按位或值 &lt;code&gt;mask&lt;/code&gt; 。当加入 &lt;code&gt;nums[r]&lt;/code&gt; 时，如果产生冲突（ &lt;code&gt;mask &amp;amp; nums[r] != 0&lt;/code&gt; ），就不断移动左指针，并把 &lt;code&gt;nums[l]&lt;/code&gt; 从 &lt;code&gt;mask&lt;/code&gt; 中删除。由于窗口中每一位只会出现一次，删除操作可以通过下面这个公式来完成：&lt;/p&gt;
&lt;p&gt;$$
mask \mathrel{\hat{=}} nums[l]
$$&lt;/p&gt;
&lt;p&gt;这样每个元素最多进窗口一次、出窗口一次，总复杂度同样是线性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;XOR相关性质&lt;/h2&gt;
&lt;p&gt;与按位与、按位或不同，按位异或运算并不具备明显的单调性，因此很多问题往往无法通过简单的 “不断变大” 或 “不断变小” 的规律来进行分析。许多复杂的异或问题，如果只从数值变化的角度出发，往往难以找到清晰的思路。&lt;/p&gt;
&lt;p&gt;不过，异或运算本身具有一些非常特殊的 &lt;strong&gt;代数结构性质&lt;/strong&gt; 。如果一组数的整体异或结果为 $x$ ，其中某一部分的异或结果为 $y$ ，那么剩余部分的异或结果一定为 $x \oplus y$ 。这一性质来源于异或的抵消性与可逆性：&lt;/p&gt;
&lt;p&gt;$$
a \oplus a = 0, \quad a \oplus 0 = a
$$&lt;/p&gt;
&lt;p&gt;也就是说，在异或运算中，整体信息可以被自由拆分，任意一部分都可以通过整体结果与另一部分的结果反推出，这使得很多问题可以从 “整体与部分” 的关系入手进行分析。&lt;/p&gt;
&lt;p&gt;这一性质在实际问题中有着非常经典的应用。例如，在一个序列中，如果除了某个元素出现奇数次之外，其余元素都出现偶数次，那么对整个序列进行一次异或运算后，所有出现偶数次的元素都会两两抵消，最终只会保留下那个出现奇数次的元素。因此可以在 $O(n)$ 的时间内直接求出答案。这类问题本质上就是利用了 “整体异或 = 各部分异或之和” 的可分解结构，将复杂的统计过程转化为一次简单的整体运算。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体讲解可以看一下左神的这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=489695542&amp;amp;bvid=BV1LN411z7nu&amp;amp;cid=1230937532&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;不仅如此，异或在某些特定结构下还会表现出明显的 &lt;strong&gt;周期性&lt;/strong&gt; 。例如定义前缀异或：&lt;/p&gt;
&lt;p&gt;$$
f(n) = 1 \oplus 2 \oplus 3 \oplus \cdots \oplus n
$$&lt;/p&gt;
&lt;p&gt;可以得到如下规律：&lt;/p&gt;
&lt;p&gt;$$
f(n) =
\begin{cases}
n &amp;amp; n \bmod 4 = 0 \
1 &amp;amp; n \bmod 4 = 1 \
n+1 &amp;amp; n \bmod 4 = 2 \
0 &amp;amp; n \bmod 4 = 3
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;也就是说，前缀异或的结果仅与 $n \bmod 4$ 有关，呈现出周期为 $4$ 的循环结构。对于任意区间 $[l, r]$ ，都有：&lt;/p&gt;
&lt;p&gt;$$
a_l \oplus a_{l+1} \oplus \cdots \oplus a_r = f(r) \oplus f(l-1)
$$&lt;/p&gt;
&lt;p&gt;从而可以在 $O(1)$ 的时间内完成区间异或的计算。&lt;/p&gt;
&lt;p&gt;这种周期性的本质来源于异或是 &lt;strong&gt;按位的模 2 加法&lt;/strong&gt;：每一位只关心出现次数的奇偶性，而在连续整数中，各二进制位的变化具有固定的循环规律，叠加之后就形成了整体的周期性表现。正因为如此，许多看似需要枚举的大规模异或问题，都可以通过发现周期规律或转化为前缀异或来快速求解。&lt;/p&gt;
&lt;h2&gt;字符串异或变换&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8763&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.luogu.com.cn/training/321766#problems&quot;&gt;【Luogu 题单】异或的艺术&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】最短路算法相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path/</guid><description>记录一些 ACM 常见题型</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;最短路径算法原理&lt;/h1&gt;
&lt;p&gt;在图论问题中，&lt;strong&gt;最短路径问题&lt;/strong&gt; 是一类极其基础且重要的研究对象。给定一张带权图，我们通常希望在众多可能的路径中找到一条总代价最小的路径。根据问题规模、图的结构以及边权的性质不同，人们设计出了多种求解最短路径的算法，例如单源最短路中的 Dijkstra 算法、Bellman–Ford 算法及其优化 SPFA，以及用于求解任意两点之间最短路的 Floyd 算法。这些算法在思想上各不相同，但本质上都是通过不断更新路径信息，逐步逼近最优解。&lt;/p&gt;
&lt;p&gt;从算法设计的角度来看，最短路径问题同时体现了 &lt;strong&gt;贪心思想&lt;/strong&gt; 与 &lt;strong&gt;动态规划&lt;/strong&gt; 两种重要范式。例如 Dijkstra 算法通过贪心策略不断确定当前最短的节点距离，而 Bellman–Ford 与 Floyd 算法则更明显地体现了动态规划的思想，通过不断进行状态转移来更新最优值。理解这些算法时，如果仅仅记住代码实现往往是不够的，更重要的是理解它们背后的建模方式和适用条件。&lt;/p&gt;
&lt;h2&gt;Dijkstra 算法&lt;/h2&gt;
&lt;p&gt;Dijkstra 算法是一种用于求解 &lt;strong&gt;单源最短路问题&lt;/strong&gt; 的经典贪心算法。给定一个带权图和源点 $s$ ，算法的目标是计算从 $s$ 到图中所有其他点的最短路径长度。算法维护一个数组 $dist[v]$ 表示当前已知的从 $s$ 到 $v$ 的最短距离估计，并使用一个优先队列按照距离从小到大选择尚未确定最短路的节点。初始时 $dist[s] = 0$ ，其余点为无穷大。每次从优先队列中取出距离最小的节点 $u$ ，此时可以认为 $dist[u]$ 已经是最终的最短距离，然后利用 $u$ 去松弛所有与其相邻的边，即尝试用 $dist[u] + w(u, v)$ 更新 $dist[v]$ 。这一过程不断重复，直到所有点的最短距离都被确定。整个算法的核心就是 &lt;strong&gt;不断贪心地确定当前距离最小的节点，并利用它更新其他节点的距离&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;Dijkstra 算法的核心正确性来自一种贪心性质：当某个节点 $u$ 被从优先队列中取出时，当前记录的 $dist[u]$ 一定已经是最短路径长度。直观理解是，如果存在一条更短的路径到达 $u$ ，那么这条路径必然经过某个尚未确定的节点，而该节点的距离应该更小，因此会优先被算法取出，从而在更早的步骤中更新 $u$ 。这种推理成立的关键前提是 &lt;strong&gt;路径代价在扩展过程中不能减少&lt;/strong&gt; ，否则就可能在之后发现更短的路径，从而破坏贪心策略。从更抽象的角度看，Dijkstra 算法 &lt;strong&gt;可以视为一种动态规划&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;设一条路径的代价为 $f(P)$ ，如果从某个状态 $A$ 通过一条边权 $w$ 扩展到状态 $B$ ，则有转移关系：&lt;/p&gt;
&lt;p&gt;$$
f(B) = F(f(A), w)
$$&lt;/p&gt;
&lt;p&gt;例如在普通最短路中，路径代价就是边权之和，此时转移函数为 $F(x, w) = x + w$ 。Dijkstra 算法实际上就是按照 $f$ 的大小顺序逐步确定这些状态的最优值，因此算法是否适用完全取决于转移函数 $F$ 的性质。&lt;/p&gt;
&lt;p&gt;首先需要满足的是单调性。由于算法依赖贪心策略，路径代价在扩展时 &lt;strong&gt;必须保持不下降&lt;/strong&gt; ，也就是：&lt;/p&gt;
&lt;p&gt;$$
F(x, w) \geq x
$$&lt;/p&gt;
&lt;p&gt;否则可能出现路径在加入新边之后反而变得更短的情况，从而使得之前已经确定的节点被后来更新，破坏算法的正确性。在普通最短路中，$F(x, w) = x + w$ ，因此要保证 $x+w\geq x$ ，就必须满足 $w \geq 0$ 。这也是为什么 Dijkstra 算法不能处理带负权边的图。&lt;/p&gt;
&lt;p&gt;其次需要满足的是 &lt;strong&gt;无后效性&lt;/strong&gt; 。如果存在两个状态 $A$ 和 $B$ ，且 $f(A) \leq f(B)$ ，那么在经过同一条边权 $w$ 扩展后，也必须保持相同的大小关系：&lt;/p&gt;
&lt;p&gt;$$
F(f(A), w) \leq F(f(B), w)
$$&lt;/p&gt;
&lt;p&gt;否则就可能出现这样一种情况：当前路径代价较小的状态在扩展之后反而变得更大，从而使得算法按照代价排序的贪心顺序被打乱。换句话说，转移函数必须保证更优的前缀路径在扩展后仍然更优，这样 Dijkstra 才能安全地按照距离从小到大确定状态。&lt;/p&gt;
&lt;p&gt;这两个条件可以用来判断某些路径代价定义是否能够使用 Dijkstra。例如将路径代价定义为所有边权的按位或：&lt;/p&gt;
&lt;p&gt;$$
f(P) = w_1 , | , w_2 , | , \dots , | , w_P
$$&lt;/p&gt;
&lt;p&gt;则转移函数为：&lt;/p&gt;
&lt;p&gt;$$
F(x, w) = x , | , w
$$&lt;/p&gt;
&lt;p&gt;这种情况下单调性是成立的，因为按位或运算只会增加或保持原有的二进制位，因此必然满足 $x , | , w \geq x$ 。然而它不满足无后效性，因为即使 $f(A) \leq f(B)$ ，也不一定有 $f(A) , | , w \leq f(B) , | , w$ 成立。按位或运算会改变数值的二进制结构，使得扩展后的大小关系可能发生变化，从而破坏 Dijkstra 依赖的贪心顺序。因此，即使路径代价始终不下降，只要无法保证状态扩展后的顺序关系不被破坏，Dijkstra 仍然无法使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 100;
const ll INF = 1e18;
struct Edge {
    int to, w;
};
vector&amp;lt;Edge&amp;gt; g[MAXN];
ll dist[MAXN];
bool vis[MAXN];
int n, m, s;

void dijkstra(int s){
    priority_queue&amp;lt;
        pair&amp;lt;ll,int&amp;gt;,
        vector&amp;lt;pair&amp;lt;ll,int&amp;gt;&amp;gt;,
        greater&amp;lt;pair&amp;lt;ll,int&amp;gt;&amp;gt;
    &amp;gt; pq;

    dist[s] = 0;
    pq.push({0, s});
    while (!pq.empty()){
        auto [d, u] = pq.top();
        pq.pop();

        if (vis[u]) continue;
        vis[u] = true;

        for (auto e : g[u]){
            int v = e.to;
            int w = e.w;

            if (dist[v] &amp;gt; dist[u] + w){
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bellman 算法&lt;/h2&gt;
&lt;p&gt;Bellman–Ford 算法是一种用于求解 &lt;strong&gt;单源最短路径问题&lt;/strong&gt; 的经典算法，与 Dijkstra 算法不同，它可以处理 &lt;strong&gt;带负权边的图&lt;/strong&gt; 。设图中有 $n$ 个点、$m$ 条边，源点为 $s$ 。我们维护一个数组 $dist[v]$ 表示当前已知的从 $s$ 到 $v$ 的最短距离估计，初始时令 $dist[s] = 0$ ，其他点为无穷大。该算法的核心思想是不断利用边进行松弛操作，如果存在这么一条边 $(u, v, w)$ 使得：&lt;/p&gt;
&lt;p&gt;$$
dist[v] &amp;gt; dist[u] + w
$$&lt;/p&gt;
&lt;p&gt;就更新这个点的距离：&lt;/p&gt;
&lt;p&gt;$$
dist[v] = dist[u] + w
$$&lt;/p&gt;
&lt;p&gt;Bellman–Ford 的关键结论是：在一个没有负环的图中，从源点到任意节点的最短路径最多只包含 $n-1$ 条边。因此，只需要把所有边按任意顺序扫描并进行松弛操作 &lt;strong&gt;$n-1$ 轮&lt;/strong&gt; ，就能够保证所有最短路径被正确计算出来。算法的正确性来自动态规划的思想：第 $k$ 轮松弛结束后，所有只包含不超过 $k$ 条边的最短路径都会被正确计算出来。经过 $n-1$ 轮之后，由于任何简单路径都不会超过 $n-1$ 条边，因此最短路一定已经确定。&lt;/p&gt;
&lt;p&gt;Bellman–Ford 还有一个重要用途是 &lt;strong&gt;检测负环&lt;/strong&gt; 。如果在完成 $n-1$ 轮松弛之后，再进行一轮松弛仍然能够更新某个节点的距离，那么说明存在一条可以无限降低路径长度的环，也就是负权环。因为如果最短路径不存在负环，那么在 $n-1$ 条边以内就已经达到最优，不可能再继续变小。&lt;/p&gt;
&lt;p&gt;Bellman–Ford 的缺点是时间复杂度较高。由于每一轮都需要遍历所有 $m$ 条边，总复杂度为 $O(nm)$ ，当图比较大时效率较低。为了减少不必要的松弛操作，人们提出了 &lt;strong&gt;SPFA（Shortest Path Faster Algorithm）&lt;/strong&gt;，可以看作是对 Bellman–Ford 的一种队列优化。&lt;/p&gt;
&lt;p&gt;SPFA 认为并不是每一轮都需要扫描所有边，只有当某个节点的距离被更新之后，它的邻接边才有可能继续产生新的松弛。因此算法维护一个队列，初始时只把源点 $s$ 放入队列。当一个节点 $u$ 被取出时，遍历所有从 $u$ 出发的边 $(u, v, w)$ ，如果发现 $dist[v] &amp;gt; dist[u] + w$ 则更新 $dist[v]$ 。如果节点 $v$ 不在队列中，就把它加入队列。这样就只会处理那些 “可能继续产生更新” 的节点，从而避免大量无效扫描。&lt;/p&gt;
&lt;p&gt;SPFA 在许多实际图上运行速度很快，平均复杂度通常接近 $O(m)$ ，但需要注意的是，它在理论上的最坏复杂度仍然是 $O(nm)$ ，甚至在某些特殊构造的图上会退化得相当严重。因此在竞赛中，如果图没有负权边，一般仍然优先使用 Dijkstra，只有在存在负权边或需要检测负环时，才会使用 Bellman–Ford 或 SPFA。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 100;
const ll INF = 1e18;
struct Edge {
    int to, w;
};
vector&amp;lt;Edge&amp;gt; g[MAXN];
ll dist[MAXN];
bool inq[MAXN];
int n, m, s;

void spfa(int s){
    queue&amp;lt;int&amp;gt; q;

    dist[s] = 0;
    q.push(s);
    inq[s] = true;
    while (!q.empty()){
        int u = q.front();
        q.pop();
        inq[u] = false;

        for (auto e : g[u]){
            int v = e.to;
            int w = e.w;

            if (dist[v] &amp;gt; dist[u] + w){
                dist[v] = dist[u] + w;

                if (!inq[v]){
                    q.push(v);
                    inq[v] = true;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Floyd-DP 算法&lt;/h2&gt;
&lt;p&gt;在很多动态规划问题中，我们经常会遇到一种非常典型的结构：&lt;strong&gt;从一个集合中逐渐选择元素，并在选择范围不断扩大的过程中维护最优解&lt;/strong&gt; 。例如经典的子序列问题，本质上就是从一个序列中选取一部分元素出来。对于序列中的每一个位置，我们都会面临一个决策：&lt;strong&gt;选这个元素，或者不选这个元素&lt;/strong&gt; 。随着处理的位置不断向后推进，我们实际上是在逐渐扩大 “允许选择的元素集合” ，而动态规划的状态正是记录在当前集合限制下的最优答案。&lt;/p&gt;
&lt;p&gt;从更抽象的角度来看，从序列中选取一部分元素，其实就是 &lt;strong&gt;从一个集合中选择若干元素组成子集&lt;/strong&gt; 。因此很多 DP 问题都可以理解为当我们逐步扩大可以使用的元素集合时，最优解如何变化。这个视角不仅适用于子序列问题，在图论问题中同样成立。&lt;strong&gt;一条路径，本质上也是从图的节点集合中选取若干节点，并按顺序连接起来形成的一条结构&lt;/strong&gt; 。如果我们逐渐扩大 “允许使用的节点集合” ，那么在这个限制下的最短路径长度也会随之发生变化。具体的思路讲解可以看一下灵神的&lt;a href=&quot;https://leetcode.cn/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/solutions/2525946/dai-ni-fa-ming-floyd-suan-fa-cong-ji-yi-m8s51/&quot;&gt;这篇题解&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;基于这个思路，我们可以把 Floyd 算法理解为一个非常自然的动态规划过程。我们定义状态 $f(k, , i, , j)$ 表示当前允许使用的节点中，编号最大的节点是 $k$ 时，从节点 $i$ 到节点 $j$ 的最短距离。这里的 $k$ 并不仅仅是一个普通的中间节点，而是 &lt;strong&gt;集合的上界&lt;/strong&gt; 。当最大的可用节点是 $k$ 时，意味着路径上所有节点都必须来自集合 ${ 1, 2, \ldots, k }$ ，因此随着 $k$ 的增加，我们实际上是在不断扩大路径允许使用的节点范围。&lt;/p&gt;
&lt;p&gt;当我们从 $k - 1$ 扩展到 $k$ 时，需要考虑路径是否会使用节点 $k$ 。如果一条最短路径不经过节点 $k$ ，那么它的所有节点仍然来自集合 ${ 1, 2, \ldots, k - 1 }$ ，因此距离不会发生变化，仍然是：&lt;/p&gt;
&lt;p&gt;$$
f(k-1, , i, , j)
$$&lt;/p&gt;
&lt;p&gt;如果路径经过节点 $k$ ，那么该路径一定可以被拆成两部分：从 $i$ 到 $k$ ，以及从 $k$ 到 $j$ 。而这两段路径的节点都只能来自集合 ${ 1, 2, \ldots, k - 1 }$ ，因为我们只允许 $k$ 作为新的最高编号节点。因此这两段路径的最短距离为：&lt;/p&gt;
&lt;p&gt;$$
f(k-1, , i, , k), \quad f(k-1, , k, , j)
$$&lt;/p&gt;
&lt;p&gt;于是就得到 Floyd 算法的状态转移关系：&lt;/p&gt;
&lt;p&gt;$$
f(k, i, j) = \min \Big(f(k-1, i, j), , f(k-1, i, k) + f(k-1, k, j) \Big)
$$&lt;/p&gt;
&lt;p&gt;从动态规划的角度来看，这个转移结构其实与子序列问题非常相似。每一步本质上都是在决定是否使用当前元素。在子序列问题中，这个元素是序列中的某个位置；而在 Floyd 算法中，这个元素则是图中的一个节点。当我们枚举 $k$ 时，本质上就是在不断扩大允许使用的节点集合，并在这个集合限制下更新所有点对之间的最短路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 505;
const ll INF = 4e18;
ll dist[MAXN][MAXN];
int n, m;

int main(){
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;
    for (int i = 1; i &amp;lt;= n; i++){
        for (int j = 1; j &amp;lt;= n; j++){
            if (i == j) dist[i][j] = 0;
            else dist[i][j] = INF;
        }
    }

    for (int i = 0; i &amp;lt; m; i++){
        int u, v; ll w;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        dist[u][v] = min(dist[u][v], w);
        dist[v][u] = min(dist[v][u], w);
    }

    for (int k = 1; k &amp;lt;= n; k++){
        for (int i = 1; i &amp;lt;= n; i++){
            for (int j = 1; j &amp;lt;= n; j++){
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;简单图最短路问题&lt;/h1&gt;
&lt;p&gt;最短路问题是图论中最基础、也是最重要的一类问题。在一张带权图中，我们通常希望找到 &lt;strong&gt;两点之间总代价最小的一条路径&lt;/strong&gt; ，而这个 “代价” 可以是距离、时间、花费或者其它抽象权值。在算法竞赛中，大量问题都可以抽象为最短路模型，因此掌握最短路算法几乎是图论学习的必经阶段。&lt;/p&gt;
&lt;p&gt;从问题结构上看，最短路问题通常分为 &lt;strong&gt;单源最短路&lt;/strong&gt; 和 &lt;strong&gt;多源最短路&lt;/strong&gt; 两类。单源最短路要求求出从一个固定起点到图中所有点的最短距离，而多源最短路则需要求任意两点之间的最短距离。针对不同类型的问题，人们设计出了多种经典算法，例如 Dijkstra、Bellman–Ford（SPFA 优化）以及 Floyd 等。这些算法虽然形式不同，但本质上都是通过不断更新路径估计值，使最短距离逐渐逼近真实答案。&lt;/p&gt;
&lt;p&gt;在算法竞赛中，很多图论题目的难点其实并不在于算法本身，而在于 &lt;strong&gt;如何将问题正确地抽象成图模型&lt;/strong&gt; 。一旦成功建图，很多问题往往就可以直接转化为标准的最短路问题求解。因此，在学习具体算法之前，理解最短路问题的基本形式以及常见建图方式，是非常重要的一步。&lt;/p&gt;
&lt;h2&gt;奶牛的旅行问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1522&quot;&gt;提取链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;农场上有 $N$ 块牧场，每块牧场有一个二维平面坐标。某些牧场之间已经存在直接路径，可以直接从一个牧场走到另一个牧场；如果两块牧场之间没有路径，则无法直接到达。这些路径使得整个图可能被分成若干个 &lt;strong&gt;互不连通的连通块&lt;/strong&gt; 。每个连通块内部的牧场都可以互相到达，但不同连通块之间无法到达。&lt;/p&gt;
&lt;p&gt;现在农夫打算 &lt;strong&gt;新建一条路径&lt;/strong&gt; ，连接两个原本不连通的牧场。新路径的长度等于两块牧场之间的 &lt;strong&gt;欧几里得距离&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;定义一个连通块的 &lt;strong&gt;直径&lt;/strong&gt; 为：该连通块中任意两块牧场之间最短路径距离的最大值。你的任务是选择一对牧场连接，使得 &lt;strong&gt;连接之后整张图的最大连通块直径尽可能小&lt;/strong&gt; 。输出这个最小可能的直径。结果保留 &lt;strong&gt;6 位小数&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 150$&lt;/li&gt;
&lt;li&gt;$-10^4 \leq x_i, y_i \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一行一个整数 $N$ ，表示牧场数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来 $N$ 行，每行两个整数 $x_i, y_i$ ，表示第 $i$ 个牧场的坐标。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来 $N$ 行，每行包含一个长度为 $N$ 的 &lt;code&gt;0/1&lt;/code&gt; 字符串：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt; 表示两个牧场之间存在路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 表示两个牧场之间没有路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$x_1 \quad y_1$&lt;/p&gt;
&lt;p&gt;$x_2 \quad y_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$x_N \quad y_N$&lt;/p&gt;
&lt;p&gt;$s_1$&lt;/p&gt;
&lt;p&gt;$s_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$s_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个浮点数，表示连接一条新路径之后的最小可能直径，结果保留 $6$ 位小数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8
10 10
15 10
20 10
15 15
20 15
30 15
25 10
30 10
01000000
10111000
01001000
01001000
01110000
00000010
00000101
00000010
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;22.071068
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;树结构中有一个非常重要的性质：如果将两棵树通过一条边连接起来，那么新树的直径端点一定来自于原来两棵树直径端点的集合。换句话说，假设两棵树的直径端点分别为 $(a,b)$ 与 $(c,d)$ ，那么新树的直径端点一定是这四个点中的任意两个。因此，新树的直径只可能有两种情况：要么仍然是原来两棵树直径中较大的那个，要么是经过新连接边的一条路径，而这条路径的两个端点一定来自原来直径端点集合中的两个点。&lt;/p&gt;
&lt;p&gt;在本题中，原图并不是一棵树，而是由若干个 &lt;strong&gt;互不连通的连通块&lt;/strong&gt; 组成的图。每一个连通块内部都可以看作一个独立的结构，其内部已经存在路径，因此每个连通块内部也同样存在一个 “直径” ，即该连通块中任意两点最短路的最大值。当我们新增一条边连接两个不同连通块时，新图的直径同样只可能来自两种来源：一种是原来各个连通块中的最大直径；另一种是 &lt;strong&gt;跨越新连接边形成的新路径&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;对于跨连接边形成的路径，其结构可以表示为：&lt;/p&gt;
&lt;p&gt;$$
dist(u,x) + w(u,v) + dist(v,y)
$$&lt;/p&gt;
&lt;p&gt;其中 $u,v$ 是新连接的两个点，$x$ 是 $u$ 所在连通块中的某个点，$y$ 是 $v$ 所在连通块中的某个点。因此这条路径的最大值取决于 $u$ 与所在连通块中最远点的距离，以及 $v$ 与其连通块最远点的距离。为了使新图的直径尽可能小，我们希望选择的连接点能够让这些最远距离尽量小。&lt;/p&gt;
&lt;p&gt;这就引出了 &lt;strong&gt;连通块中心点&lt;/strong&gt; 的概念。对于一个连通块中的某个点 $p$ ，定义：&lt;/p&gt;
&lt;p&gt;$$
ecc(p) = \max_{v} dist(p,v)
$$&lt;/p&gt;
&lt;p&gt;也就是从该点到连通块中最远节点的最短路距离。如果我们在所有点中选择使 $ecc(p)$ 最小的点，那么这个点就可以看作该连通块的 &lt;strong&gt;中心点&lt;/strong&gt; 。直观上理解，中心点就是整个连通块最 “居中” 的位置，从这里向外扩展时，最远距离最小。因此，当我们需要在两个连通块之间添加一条边时，连接两个连通块的中心点能够使跨边路径的长度最小。&lt;/p&gt;
&lt;p&gt;为了求出这些距离信息，我们首先需要得到图中 &lt;strong&gt;任意两点之间的最短距离&lt;/strong&gt; 。由于题目规模较小，可以直接使用 Floyd 算法进行全源最短路计算。Floyd 算法能够在 $O(n^3)$ 的时间内求出所有点对之间的最短路径，这样我们就可以方便地获得任意两点之间的距离。&lt;/p&gt;
&lt;p&gt;当所有最短路计算完成后，我们就可以对每个点计算其 $ecc$ 值，也就是它到所在连通块中最远点的距离。同时还可以求出每个连通块当前的直径。接下来枚举两个原本不连通的点 $u,v$ ，假设在它们之间新建一条边，其长度为两点之间的欧几里得距离，那么通过这条边形成的新路径长度为：&lt;/p&gt;
&lt;p&gt;$$
ecc(u) + dist(u,v) + ecc(v)
$$&lt;/p&gt;
&lt;p&gt;此时新图的直径就是原来各连通块直径的最大值与这条跨边路径长度之间的较大者。我们枚举所有可能的连接方式，并取其中直径最小的方案作为答案即可。&lt;/p&gt;
&lt;p&gt;整体算法流程可以概括为：先使用 Floyd 求出所有点对最短路，然后计算每个点的最远距离以及各连通块的直径，最后枚举不同连通块之间的连边方案，计算新图的直径并取最小值。由于 $n \leq 150$ ，Floyd 的复杂度是可以接受的，因此这种直接枚举的方法能够顺利通过本题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;奶牛的接力比赛&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2886&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一张无向带权图，图中有若干个路口以及连接它们的道路。每条道路连接两个不同的路口，并且具有一个长度。保证任意两点之间不会存在两条不同的直接道路。&lt;/p&gt;
&lt;p&gt;现在有 $N$ 头牛需要进行接力跑。接力过程中，每一头牛都会沿着一条道路跑到下一个路口，然后将接力棒交给下一头牛。换句话说，整个过程会 &lt;strong&gt;恰好经过 N 条道路&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给定起点 $S$ 和终点 $E$ ，请你求出 &lt;strong&gt;从 S 到 E 恰好经过 N 条道路的最短路径长度&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq N \leq 10^6$&lt;/li&gt;
&lt;li&gt;$2 \leq T \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq length_i \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq I_{1i}, I_{2i} \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行四个整数 $N, T, S, E$ 。&lt;/li&gt;
&lt;li&gt;接下来 $T$ 行，每行三个整数 $length_i, I_{1i}, I_{2i}$ ，表示存在一条长度为 $length_i$ 的道路连接路口 $I_{1i}$ 和 $I_{2i}$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad T \quad S \quad E$&lt;/p&gt;
&lt;p&gt;$length_1 \quad I_{11} \quad I_{21}$&lt;/p&gt;
&lt;p&gt;$length_2 \quad I_{12} \quad I_{22}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$length_T \quad I_{1T} \quad I_{2T}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示从 $S$ 到 $E$ 恰好经过 $N$ 条道路的最短路径长度。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 6 6 4
11 4 6
4 4 8
8 4 9
6 6 8
2 6 9
3 8 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;对于 &lt;strong&gt;恰好走 k 步&lt;/strong&gt; 的路径问题，一个非常常见的建模技巧是 &lt;strong&gt;分层图&lt;/strong&gt; 。具体做法是将原图复制 $k$ 层，第 $i$ 层表示已经走了 $i$ 步的状态。如果原图中存在一条边 $u \to v$ ，那么就在第 $i$ 层向第 $i+1$ 层连一条边 $(u,i) \to (v,i+1)$ ，边权与原图相同。这样一来，从起点状态 $(S,0)$ 出发，到达终点状态 $(E,k)$ 的最短路径，就对应着 &lt;strong&gt;恰好走 k 条边&lt;/strong&gt; 的最短路问题。建图完成后，只需要在这张扩展图上跑一次最短路即可。&lt;/p&gt;
&lt;p&gt;然而在本题中，题目给出的 $N$ 非常大。如果按照分层图的方式建图，就需要构造 $N$ 层图结构，图的规模将会膨胀到原来的 $N$ 倍，无论是空间还是时间都无法接受，因此这种直接扩图的方法在这里并不可行。&lt;/p&gt;
&lt;p&gt;观察最短路的转移方式，可以得到一个更本质的递推关系。设 $f_k(i,j)$ 表示从节点 $i$ 出发，&lt;strong&gt;恰好经过 k 条边&lt;/strong&gt; 到达节点 $j$ 的最短距离。若最后一步是从某个节点 $t$ 走到 $j$ ，那么有：&lt;/p&gt;
&lt;p&gt;$$
f_k(i,j) = \min_t \big( f_{k-1}(i,t) + w(t,j) \big)
$$&lt;/p&gt;
&lt;p&gt;这个转移形式与矩阵乘法非常相似。对比普通矩阵乘法：&lt;/p&gt;
&lt;p&gt;$$
C_{ij} = \sum_k A_{ik} \times B_{kj}
$$&lt;/p&gt;
&lt;p&gt;而这里的运算则要修改成如下形式：&lt;/p&gt;
&lt;p&gt;$$
C_{ij} = \min_k (A_{ik} + B_{kj})
$$&lt;/p&gt;
&lt;p&gt;这种运算被称为 &lt;strong&gt;min-plus 矩阵乘法&lt;/strong&gt; 。如果把 $A_{ij}$ 定义为从 $i$ 到 $j$ &lt;strong&gt;恰好走一条边&lt;/strong&gt; 的最短距离，那么通过一次 min-plus 乘法就可以得到 &lt;strong&gt;恰好走两条边&lt;/strong&gt; 的最短距离，继续进行乘法，就可以得到更多步数的结果。&lt;/p&gt;
&lt;p&gt;因此，从 $i$ 到 $j$ 恰好走 $N$ 条边的最短路，本质上等价于对这个权值矩阵进行 &lt;strong&gt;N 次 min-plus 乘法&lt;/strong&gt; 。直接做 $N$ 次乘法显然不可行，我们可以使用 &lt;strong&gt;矩阵快速幂&lt;/strong&gt; 的思想，将复杂度从 $O(N)$ 次乘法降低到 $O(\log N)$ 次乘法。&lt;/p&gt;
&lt;p&gt;最终的算法流程就是：首先构造一个矩阵 $A$ ，其中 $A_{ij}$ 表示从 $i$ 到 $j$ 经过一条边的最短距离，然后通过 &lt;strong&gt;min-plus 矩阵快速幂&lt;/strong&gt; 计算 $A^N$ 。矩阵 $A^N$ 中的第 $S,E$ 个元素，就表示从 $S$ 出发恰好经过 $N$ 条边到达 $E$ 的最短路径长度。由于图中实际出现的节点数量不超过一百个左右，因此矩阵规模较小，整体复杂度约为 $O(n^3 \log N)$ ，可以顺利通过本题的数据范围。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;旅游运营的烦恼&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P10947&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;旅游运营商 &lt;em&gt;Your Personal Holiday&lt;/em&gt; 组织前往荷卢经济联盟的导游巴士旅行。每天，巴士都会从一个城市 $S$ 行驶到另一个城市 $F$ 。在路线上，游客可以看到沿途的景点。此外，公共汽车会在一些美丽的城市停靠，游客可以在那里下车欣赏当地的风光。&lt;/p&gt;
&lt;p&gt;由于不同游客群体对景点的偏好不同，从 $S$ 到 $F$ 的路线也有不同的喜好。如果两条路线中至少有一条道路不属于另一条路线，则认为这两条路线是不同的。&lt;/p&gt;
&lt;p&gt;为了保证游客有足够的观光时间并避免浪费燃料，公共汽车必须选择一条 &lt;strong&gt;最短路径&lt;/strong&gt; ，或者一条 &lt;strong&gt;比最短路径长度恰好长 1 个单位&lt;/strong&gt; 的路线。现在给定城市地图以及出发城市 $S$ 和终点城市 $F$ ，请计算有多少条符合条件的路线。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 100$&lt;/li&gt;
&lt;li&gt;$2 \leq N \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq M \leq 10000$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i, B_i, S, F \leq N$&lt;/li&gt;
&lt;li&gt;$1 \leq W_i \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$case_1$&lt;/p&gt;
&lt;p&gt;$case_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$case_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，表示城市数量以及道路数量。&lt;/li&gt;
&lt;li&gt;接下来的 $M$ 行，每行包含三个整数 $A_i$ 、$B_i$ 和 $W_i$ 表示道路。&lt;/li&gt;
&lt;li&gt;最后一行包含两个整数 $S$ 和 $F$ 表示出发城市和终点城市。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$A_1 \quad B_1 \quad W_1$&lt;/p&gt;
&lt;p&gt;$A_2 \quad B_2 \quad W_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_M \quad B_M \quad W_M$&lt;/p&gt;
&lt;p&gt;$S \quad F$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个测试用例，输出一行。该行包含一个整数，表示长度等于最短路或最短路 $+1$ 的路径总数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
5 8 
1 2 3 
1 3 2 
1 4 5 
2 3 1 
2 5 3 
3 4 2 
3 5 4 
4 5 3 
1 5
5 6
2 3 1
3 2 1
3 1 10
4 5 2 
5 2 7 
5 2 7 
4 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的本质是一个 &lt;strong&gt;带约束的最短路计数问题&lt;/strong&gt; 。题目要求我们不仅要统计从起点 $S$ 到终点 $F$ 的最短路径条数，还要统计比最短路径长度恰好大 $1$ 的路径条数。由于图中的边权均为正整数，我们可以将其转化为在扩展状态空间下的最短路搜索模型。&lt;/p&gt;
&lt;p&gt;首先考虑基础的 &lt;strong&gt;最短路计数&lt;/strong&gt; 。在标准的 Dijkstra 算法中，我们通过松弛操作更新起点到每个节点 $i$ 的最短距离 &lt;code&gt;dist[i]&lt;/code&gt; 。为了统计条数，我们可以引入一个计数数组 &lt;code&gt;cnt[i]&lt;/code&gt; 。当发现一条更短的路径时，我们不仅更新 &lt;code&gt;dist[i]&lt;/code&gt; ，还要将 &lt;code&gt;cnt[i]&lt;/code&gt; 覆盖为前驱节点的计数；当发现一条路径长度等于当前的 &lt;code&gt;dist[i]&lt;/code&gt; 时，则将前驱节点的计数累加到 &lt;code&gt;cnt[i]&lt;/code&gt; 中。这种做法利用了最短路路径的 &lt;strong&gt;最优子结构性质&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;对于本题要求的最短路与次短路，我们需要进一步扩展状态。由于我们要找的是特定长度的路径，可以为每个节点维护两个独立的状态：&lt;code&gt;dist[i][0]&lt;/code&gt; 表示到达节点 $i$ 的 &lt;strong&gt;最短距离&lt;/strong&gt; ，&lt;code&gt;dist[i][1]&lt;/code&gt; 表示到达节点 $i$ 的 &lt;strong&gt;次短距离&lt;/strong&gt; 。相应地，我们也需要两个计数数组 &lt;code&gt;cnt[i][0]&lt;/code&gt; 和 &lt;code&gt;cnt[i][1]&lt;/code&gt; 。在 Dijkstra 的堆优化过程中，每次从优先队列取出当前距离最小的状态 $(u, d, type)$ ，并尝试更新其邻接节点 $v$ 。&lt;/p&gt;
&lt;p&gt;在更新节点 $v$ 时，设当前路径长度为 $W = d + weight(u, v)$ ，会出现以下四种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;$W &amp;lt; dist[v][0]$&lt;/strong&gt;：找到了更短的路径，此时原本的最短路降级为次短路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$W == dist[v][0]$&lt;/strong&gt;：找到了另一条长度相等的最短路，直接累加计数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$W &amp;lt; dist[v][1]$&lt;/strong&gt;：长度介于最短路和次短路之间，此时更新次短路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$W == dist[v][1]$&lt;/strong&gt;：找到了另一条长度相等的次短路，直接累加计数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后，我们需要根据计算结果判断哪些路径符合最短路 $+1$ 的条件。在算法结束时，&lt;code&gt;cnt[F][0]&lt;/code&gt; 始终是我们要找的最短路径条数。如果计算出的次短路长度 &lt;code&gt;dist[F][1]&lt;/code&gt; 恰好满足 &lt;code&gt;dist[F][0] + 1&lt;/code&gt; 这个条件，那么最终答案就是 &lt;code&gt;cnt[F][0] + cnt[F][1]&lt;/code&gt; ；否则，次短路不符合要求，答案仅为 &lt;code&gt;cnt[F][0]&lt;/code&gt; 。这种状态拆分的方法保证了我们在搜索过程中能够完整保留所需的路径信息，且依然满足 Dijkstra 算法的贪心选择性质。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nya的层级迷宫&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://acm.hdu.edu.cn/showproblem.php?pid=4725&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;由于 Nya Graph 的特殊性，在这个图上寻找最短路径是一个有趣的问题。Nya Graph 包含 $N$ 个顶点，且每个顶点 $i$ 都属于一个特定的层 $L_i$（ $1 \leq L_i \leq N$ ）。&lt;/p&gt;
&lt;p&gt;图中的边由以下规则构成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;层间边&lt;/strong&gt;：如果第 $i$ 层和第 $j$ 层满足 $|i - j| = 1$ ，则第 $i$ 层的所有顶点与第 $j$ 层的所有顶点之间都存在一条权重为 $C$ 的双向边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;额外边&lt;/strong&gt;：图中还存在 $M$ 条直接连接顶点 $u$ 和 $v$ 的双向边，权重为 $w$ 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在给定起点 $S$ 和终点 $F$ ，请计算从 $S$ 到 $F$ 的最短路径长度。如果无法到达，则输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 100$&lt;/li&gt;
&lt;li&gt;$2 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq M \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq C, w \leq 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq L_i, S, F, u, v \leq N$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$case_1$&lt;/p&gt;
&lt;p&gt;$case_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$case_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$M$ 和 $C$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数表示每个顶点的所属层数。&lt;/li&gt;
&lt;li&gt;接下来的 $M$ 行，每行包含三个整数表示边。&lt;/li&gt;
&lt;li&gt;最后一行包含两个整数 $S$ 和 $F$ ，表示起点和终点。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M \quad C$&lt;/p&gt;
&lt;p&gt;$L_1 \quad L_2 \quad \ldots \quad L_N$&lt;/p&gt;
&lt;p&gt;$A_1 \quad B_1 \quad W_1$&lt;/p&gt;
&lt;p&gt;$A_2 \quad B_2 \quad W_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_M \quad B_M \quad W_M$&lt;/p&gt;
&lt;p&gt;$S \quad F$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个测试用例，输出格式为 &lt;code&gt;Case #x: y&lt;/code&gt; ，其中 &lt;code&gt;x&lt;/code&gt; 是测试用例编号，&lt;code&gt;y&lt;/code&gt; 是最短路径长度或 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
3 3 3
1 3 2
1 2 1
2 3 1
1 3 3
1 3
3 3 3
1 3 2
1 2 2
2 3 2
1 3 4
1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Case #1: 2
Case #2: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;虚点建图&lt;/p&gt;
&lt;h2&gt;地区的灾后重建&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1119&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;机场的扩建计划&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc416/tasks/abc416_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;分层图最短路问题&lt;/h1&gt;
&lt;p&gt;分层图最短路问题，本质上是一种通过扩充状态来刻画过程的建模方法。在许多实际场景中，原图中的单一节点无法完整描述当前的实时状态，例如剩余的资源量、特殊技能的使用次数或者所处的任务阶段。如果直接在原图求解，会导致决策信息的丢失。因此我们引入 &lt;strong&gt;分层思想&lt;/strong&gt; ，将单一节点拆解为多个携带状态标签的副本。这样一来，路径上的所有决策信息都被显式地记录下来，问题就转化成一个更大的图上的最短路问题。&lt;/p&gt;
&lt;p&gt;从底层逻辑来看，&lt;strong&gt;分层图和动态规划非常相似&lt;/strong&gt; 。动态规划通过增加状态维度，使得每一个状态都能完整表述一个独立的子问题。分层图则是将这种思维具象化，通过增加图的层数，使得每一个节点都能精确对应一个位置与状态的组合。然而 &lt;strong&gt;状态扩展并非越多越好&lt;/strong&gt; ，理论上将所有可能的信息全部纳入状态即可覆盖所有合法方案，但代价往往是状态总数呈指数级增长，导致时间和空间复杂度超出算法承受的范围。因此分层图建模的关键在于找到一个恰到好处的点：状态设计需要足够丰富以保证逻辑的正确性，同时又要足够精简以确保图的规模在可控范围内。&lt;/p&gt;
&lt;p&gt;在实际建模时，我们通常围绕路径上的限制条件展开思考。无论是允许使用有限次数的特殊技能，还是要求路径长度必须符合特定的模数性质，每增加一个约束往往就对应一个新的维度。例如最多使用一次免费通行可以扩充为两层图，而当前距离模某个数的余数则可以扩充为若干层。这些层与层之间通过符合题目逻辑的特定规则连边，从而精确模拟了整个决策过程。当状态设计得恰到好处时，原本错综复杂的路径选择就会自然地转化为一个层次分明且清晰可解的最短路模型。&lt;/p&gt;
&lt;p&gt;总结来说，分层图是一种通过 “增加状态维度” 来换取 “问题结构清晰化” 的方法。它的思想和动态规划一脉相承，本质上是 &lt;strong&gt;用空间换时间、用结构化暴力换取确定性解法&lt;/strong&gt; 。掌握分层图的关键，不在于记住某种固定模板，而在于学会分析问题的本质约束，并设计出既完整又高效的状态空间。当状态设计得恰到好处时，原本复杂的路径决策问题，就会自然地转化为一个清晰可解的最短路模型。&lt;/p&gt;
&lt;h2&gt;异或最短路问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc410/tasks/abc410_d&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个有 $N$ 个顶点和 $M$ 条边的有向图。顶点编号为 $1$ 到 $N$ ，边编号为 $1$ 到 $M$ 。第 $i$ 条边是从顶点 $A_i$ 指向顶点 $B_i$ 的有向边，其权值为 $W_i$ 。&lt;/p&gt;
&lt;p&gt;请你求出从顶点 $1$ 到顶点 $N$ 的所有 &lt;strong&gt;walk&lt;/strong&gt; 中，路径经过的所有边的边权的 &lt;strong&gt;异或和&lt;/strong&gt; 的最小值。&lt;/p&gt;
&lt;p&gt;所谓从顶点 $1$ 到顶点 $N$ 的 &lt;strong&gt;walk&lt;/strong&gt; ，是指满足以下条件的边序列 $(e_1, e_2, \ldots, e_k)$ ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$e_1$ 的起点是顶点 $1$ 。&lt;/li&gt;
&lt;li&gt;对于所有 $1 \leq i &amp;lt; k$ ，边 $e_i$ 的终点与边 $e_{i+1}$ 的起点相同。&lt;/li&gt;
&lt;li&gt;$e_k$ 的终点是顶点 $N$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：walk 可以多次经过同一个顶点或同一条边。如果不存在从顶点 $1$ 到顶点 $N$ 的 walk，则输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq N \leq 1000$&lt;/li&gt;
&lt;li&gt;$0 \leq M \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i, B_i \leq N$&lt;/li&gt;
&lt;li&gt;$0 \leq W_i &amp;lt; 2^{10}$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，表示顶点数和边数。&lt;/li&gt;
&lt;li&gt;接下来 $M$ 行，每行包含三个整数 $A_i$ 、$B_i$ 和 $W_i$ ，表示一条从 $A_i$ 到 $B_i$ 且权值为 $W_i$ 的有向边。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$A_1 \quad B_1 \quad W_1$&lt;/p&gt;
&lt;p&gt;$A_2 \quad B_2 \quad W_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_M \quad B_M \quad W_M$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果存在从顶点 $1$ 到顶点 $N$ 的 walk，输出所有可能路径中边权异或和的最小值；否则输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
1 2 4
2 3 5
1 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 4
1 4 7
4 2 2
2 3 4
3 4 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;动态边权最短路&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc422/tasks/abc422_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有一个连通的 &lt;strong&gt;无向图&lt;/strong&gt; ，有 &lt;code&gt;N&lt;/code&gt; 个顶点和 &lt;code&gt;M&lt;/code&gt; 条边。顶点编号是从 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;N&lt;/code&gt; 。图中每条边连接两个顶点。初始时，高桥君的体重为 $0$ 。高桥君骑车从顶点 $1$ 出发，目标是到达每一个顶点。&lt;/p&gt;
&lt;p&gt;当他访问某个顶点 $v$ 时，他的体重会增加 $W_v$ 。当他骑车沿一条边移动时，车消耗的燃料等于他此时的体重。&lt;/p&gt;
&lt;p&gt;你需要计算：对于每个顶点 $i$ ，高桥君到达 $i$ 时消耗的最小燃料。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 5000$&lt;/li&gt;
&lt;li&gt;$1 \leq M \leq 5000$&lt;/li&gt;
&lt;li&gt;$1 \leq W_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，分别表示点和边的个数。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数表示每个顶点的权值。&lt;/li&gt;
&lt;li&gt;接下来 $M$ 行都会给出两个整数，表示这两个顶点之间存在一条边与之相连。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$W_1 \quad W_2 \quad \ldots \quad W_N$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_M \quad v_M$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;在第 $i$ 行输出一个整数，表示从顶点 $1$ 出发到达顶点 $i$ 所需消耗的最小燃料。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 6
3 1 4 1 5
1 2
1 3
2 3
2 4
3 5
4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
3
3
7
10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 4
1000000000 1000000000 1000000000 1000000000 1000000000
1 2
2 3
3 4
4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
1000000000
3000000000
6000000000
10000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;Dijkstra 之所以能够成立，本质是因为它依赖于 DP 的无后效性。最短路问题通常默认有：&lt;/p&gt;
&lt;p&gt;$$
\mathrm{dist}[C] = \mathrm{dist}[B] + \mathrm{cost}(B \to C)
$$&lt;/p&gt;
&lt;p&gt;其中转移代价 $\mathrm{cost}(B \to C)$ 仅依赖于当前状态 $B$ ，而与到达 $B$ 的具体路径无关。因此一旦某个顶点 $B$ 的最优距离被确定，则从该顶点出发的所有后续扩展，其代价结构应当是固定且独立于历史路径的。在这一条件下，贪心地锁定当前最小距离顶点才具有理论保证。&lt;/p&gt;
&lt;p&gt;显然本题并不满足上述条件。题目中的总代价并非简单的边权累加，而是与路径中各顶点所处的位置相关。具体而言，路径长度的不同会导致同一顶点在不同路径中承担不同的权重贡献。因此，即使两条路径到达同一个顶点，其后续扩展的代价模型也可能不同。这意味着，在原图中仅以 $\mathrm{dist}[v]$ 作为状态是不充分的，因为该状态未记录 “已行进步数” 这一影响后续代价的关键信息。&lt;/p&gt;
&lt;p&gt;为了恢复无后效性，可以从&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/enumeration/#%E5%AF%B9%E8%B1%A1%E4%BA%A4%E6%8D%A2%E8%B4%A1%E7%8C%AE%E6%B3%95&quot;&gt;对象转化贡献法&lt;/a&gt;的角度对问题重新建模。若路径上的某个顶点距离终点还剩 $k$ 步，则其对总代价的贡献为 $k W$ ，其中 $W$ 为该顶点的权值。于是，整条路径的总代价可表述为所有顶点权值在其 “剩余步数” 意义下的加权求和。问题因此转化为在所有合法路径中，寻找一条使上述加权和最小的路径。&lt;/p&gt;
&lt;p&gt;构造新的状态空间 $(v, k)$ ，其中 $v$ 表示当前顶点，$k$ 表示到达终点尚需的步数。若存在原图边 $u \to v$ ，则在扩展图中引入转移 $(u, k) \to (v, k-1)$ ，并赋予其权值 $k W_u$ 。在该建模下，每条边的权值成为确定常数，不再依赖更早的路径历史，状态描述亦趋于完整。由于第二维 $k$ 在转移过程中单调递减，扩展图构成一张有向无环图，从而可以按拓扑顺序进行求解，其时间复杂度为 $O(NM)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跳跃的旅行问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc414/tasks/abc414_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你被给定一棵包含 &lt;code&gt;N&lt;/code&gt; 个顶点的 &lt;strong&gt;树&lt;/strong&gt;（无向连通无环图）以及一个整数 &lt;code&gt;K&lt;/code&gt; 。顶点编号为 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;N&lt;/code&gt; 。每条边连接两个顶点，并且所有边的距离均为 &lt;code&gt;1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;你现在位于顶点 &lt;code&gt;1&lt;/code&gt; 。可以重复执行以下操作 &lt;strong&gt;0 次或更多次&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;选择一个与当前所在顶点的 &lt;strong&gt;距离恰好等于 K&lt;/strong&gt; 的顶点，并移动到该顶点。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这里两个顶点之间的距离定义为连接它们的简单路径上的边数。&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于每个 $k = 2, 3, \ldots, N$ ，判断是否可以通过若干次上述操作从顶点 &lt;code&gt;1&lt;/code&gt; 移动到顶点 &lt;code&gt;k&lt;/code&gt; ；如果可以，请输出实现该目的所需的最少操作次数，否则输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 10^5$&lt;/li&gt;
&lt;li&gt;$2 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq K \leq 20$&lt;/li&gt;
&lt;li&gt;$1 \leq u_i &amp;lt; v_i \leq N$&lt;/li&gt;
&lt;li&gt;所有测试用例中 $N$ 的总和不超过 $2 \times 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$case_1$&lt;/p&gt;
&lt;p&gt;$case_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$case_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $K$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $N - 1$ 行，每行包含两个整数，表示一条边的两个顶点。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad K$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_{N-1} \quad v_{N-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出共 $T$ 行，每行输出对应个数的整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
6 2
1 2
2 3
2 4
4 5
5 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1 1 1 -1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2 20
1 2
10 2
1 9
1 8
1 5
6 8
4 5
2 8
5 10
7 9
3 5
10 1
2 6
2 9
8 9
9 10
3 9
4 9
7 9
1 6
3 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
1 1 1 -1 1 1 -1 -1 1
2 4 4 5 1 4 4 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的核心在于理解 &lt;strong&gt;一次操作的本质&lt;/strong&gt; 。题目允许我们从当前顶点出发，移动到距离恰好为 $K$ 的顶点，并且可以重复若干次。若直接从定义出发，很自然会想到构造一个 &lt;strong&gt;K 步图&lt;/strong&gt;：对于原树中的每个顶点 $v$ ，将所有与 $v$ 距离为 $K$ 的顶点连边，然后在这张新图上做最短路，答案即为最少跳跃次数。从理论上讲，这种思路完全正确，甚至可以用 &lt;strong&gt;邻接矩阵快速幂&lt;/strong&gt; 来刻画 “恰好走 $K$ 步” 的可达关系。然而，矩阵乘法的复杂度过高，在 $N$ 达到 $2\times 10^5$ 量级时显然无法承受，因此我们不能显式构造这张 $K$ 步图。&lt;/p&gt;
&lt;p&gt;关键转折在于 &lt;strong&gt;对操作进行拆解&lt;/strong&gt; ，一次跳跃本质上是沿树边连续走 $K$ 步。因此，与其一次性考虑 “走到距离为 $K$ 的点” ，不如把问题转化为：在树上按边逐步行走，同时记录当前已经走了多少步。当累计步数达到 $K$ 时，视为完成一次操作，并重新开始计数。这样，我们就不再需要构造 $K$ 步图，而是在原图的基础上引入阶段维度。&lt;/p&gt;
&lt;p&gt;为此，我们构造分层图状态 $(v, k)$ ，其中 $v$ 表示当前所在顶点，$k$ 表示当前总步数对 $K$ 取模的结果。从状态 $(v, k)$ 出发，可以沿原树中的任意一条边走到相邻顶点 $u$ ，并转移到状态 $(u, (k+1) \bmod K)$ 。当 $k+1 \equiv 0 \pmod K$ 时，说明恰好完成了一次 &lt;strong&gt;长度为 K 的连续移动&lt;/strong&gt; ，此时我们记录到达该顶点所需的操作次数为当前总步数除以 $K$ 。整个过程可以用 BFS 实现，因为每走一条边的代价为 $1$ ，BFS 能保证首次到达某个 “完成跳跃” 的状态时，其操作次数最小。&lt;/p&gt;
&lt;p&gt;如果仅仅构造这样的分层图，仍可能出现复杂度过高的问题。因为在树中，每个顶点可能有较高的度数，而同一个 $(v, k)$ 状态可能从不同方向多次到达，从而重复扩展邻边。若不加限制，最坏情况下会退化为 $O(N^2K)$。&lt;/p&gt;
&lt;p&gt;优化的关键在于利用 &lt;strong&gt;树的结构&lt;/strong&gt; 。注意到在一次尚未完成的跳跃过程中（即 $k \neq 0$ 时），我们不允许立即沿刚才走过的边原路返回，否则相当于在一条简单路径中产生回溯。由于 &lt;strong&gt;树是无环图&lt;/strong&gt; ，若禁止中途回头，那么在一次跳跃内部，路径必然是一条 &lt;strong&gt;简单路径&lt;/strong&gt; 。这样一来，对于固定的 $(v, k)$ ，从结构上讲，最多只会有两种不同方向的到达方式，第三次到达时所能产生的后续转移，必然已经在前两次扩展中覆盖。因此，我们可以对每个 $(v, k)$ 只允许 &lt;strong&gt;处理至多两次&lt;/strong&gt; ，从而避免重复扩展。&lt;/p&gt;
&lt;p&gt;在实现上，可以额外记录每个 $(v, k)$ 被访问的次数，当次数达到 $2$ 时停止继续扩展。由于树中边数为 $N-1$ ，所有状态数为 $N \times K$ ，每个状态至多扩展常数次，整体时间复杂度便可控制在 $O(NK)$ 。在题目给定的 $K \leq 20$ 条件下，这是完全可行的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;操作次数最短路&lt;/h1&gt;
&lt;p&gt;在算法竞赛与理论计算机科学中，&lt;strong&gt;最少操作次数问题的本质是状态图的最短路&lt;/strong&gt; 。在这类问题中，我们将每一种可能的情况 &lt;strong&gt;抽象为图中的顶点&lt;/strong&gt; ，将每一次操作 &lt;strong&gt;抽象为连接状态的有向边&lt;/strong&gt; 。若每次操作代价相等，则问题转化为初始状态到目标状态的 &lt;strong&gt;BFS 搜索&lt;/strong&gt;；若操作代价各异，则对应为 &lt;strong&gt;带权最短路问题&lt;/strong&gt; 。这种建模方式将离散的动态决策过程形式化为图上的路径搜索，从而使标准算法能够直接应用于原本抽象的逻辑推导。&lt;/p&gt;
&lt;p&gt;然而这种建模方式的核心挑战并不在算法本身，而在于 &lt;strong&gt;状态空间的规模控制&lt;/strong&gt; 。理论上只要状态与转移规则明确，任何操作序列都能构造成图，但在实践中状态数往往随变量增加呈 &lt;strong&gt;指数级增长&lt;/strong&gt; 。因此，这种建模方式的可行性并不取决于算法本身，而在于能否通过深入分析问题的约束，将 &lt;strong&gt;状态规模压缩至可控范围内&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;从实践角度看，这类问题的关键在于对状态空间的 &lt;strong&gt;精细分析与结构化审题&lt;/strong&gt; 。一方面，我们需要通过寻找 &lt;strong&gt;等价状态、冗余转移&lt;/strong&gt; 或利用问题的 &lt;strong&gt;对称性&lt;/strong&gt; 来进行剪枝，从而将理论上的状态上界压缩为实际可行的搜索规模。另一方面，在面对 &lt;strong&gt;最少&lt;/strong&gt; 与 &lt;strong&gt;操作&lt;/strong&gt; 这类关键词时，应当形成一种敏锐的 &lt;strong&gt;条件反射&lt;/strong&gt; 。只要经估算状态空间在承受范围内，这种将逻辑推导转化为图论搜索的 &lt;strong&gt;升维建模&lt;/strong&gt; ，往往比复杂的贪心证明或动态规划更为直接可靠，也是许多看似棘手问题的标准解法。&lt;/p&gt;
&lt;h2&gt;高桥君清理垃圾&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc427/tasks/abc427_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个 $H \times W$ 的网格，高桥君位于其中某一格，网格中的一些格子上有垃圾。状态由 $H$ 个长度为 $W$ 的字符串 $S_1,\ldots,S_H$ 表示，其中 $S_{i,j}$ 的含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $S_{i,j} =$ &lt;code&gt;T&lt;/code&gt;，则格子 $(i,j)$ 是高桥君所在的位置。&lt;/li&gt;
&lt;li&gt;如果 $S_{i,j} =$ &lt;code&gt;#&lt;/code&gt;，则格子 $(i,j)$ 有垃圾。&lt;/li&gt;
&lt;li&gt;如果 $S_{i,j} =$ &lt;code&gt;.&lt;/code&gt;，则格子 $(i,j)$ 是空的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，高桥君所在的格子上没有垃圾。每次可以选择四个方向之一（上、下、左、右），并同时将所有垃圾向该方向移动一格。如果垃圾移出网格则消失；如果垃圾移动到高桥君所在的位置，则高桥君会被弄脏。&lt;/p&gt;
&lt;p&gt;请判断高桥君是否可以在不弄脏自己的情况下，通过若干次操作让所有垃圾消失，如果可以，求使所有垃圾消失的最小操作次数；否则输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq H, W \leq 12$&lt;/li&gt;
&lt;li&gt;每个 $S_i$ 是长度为 $W$ 的由 &lt;code&gt;T&lt;/code&gt; , &lt;code&gt;#&lt;/code&gt; , &lt;code&gt;.&lt;/code&gt; 组成的字符串&lt;/li&gt;
&lt;li&gt;恰有一个格子上为 &lt;code&gt;T&lt;/code&gt; ，至少有一个格子上为 &lt;code&gt;#&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $H$ 和 $W$ 。&lt;/li&gt;
&lt;li&gt;接下来 $H$ 行，每行包含一个长度为 $W$ 的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$H \quad W$&lt;/p&gt;
&lt;p&gt;$S_1$&lt;/p&gt;
&lt;p&gt;$S_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$S_H$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果可以不弄脏高桥君地情况下让所有垃圾消失，则输出所需的最少操作次数；否则输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 4
###.
#.T#
...#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
###
#T#
###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 5
###..
#T...
..##.
##..#
#..#.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/difference-constraints/&quot;&gt;【ACM 算法题单】差分约束相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-modular/&quot;&gt;【ACM 算法题单】同余最短路相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/shortest-path/shortest-path-tree/&quot;&gt;【ACM 算法题单】最短路树相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】数位动态规划相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-classification/digit-dp/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-classification/digit-dp/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数位动态规划问题&lt;/h1&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Brilliant11001/p/18389858&quot;&gt;【会咬人的氯化氢】数位DP题目合集&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/-ytz/p/16636155.html&quot;&gt;【yangtz】数位DP题目详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】状压动态规划相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-classification/state-dp/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-classification/state-dp/</guid><description>记录一些 ACM 常见题型</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;状压动态规划问题&lt;/h1&gt;
&lt;h3&gt;多进制状态设计&lt;/h3&gt;
&lt;h2&gt;顺序分配问题&lt;/h2&gt;
&lt;p&gt;记得与阶乘枚举做区分（注意 旅行商问题 跟 dijkstra 之间的关系）&lt;/p&gt;
&lt;h2&gt;集合分配问题&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;棋盘轮廓状压问题&lt;/h1&gt;
&lt;p&gt;轮廓线dp就是专门为棋盘类题目准备的&lt;/p&gt;
&lt;h3&gt;网格连通性问题&lt;/h3&gt;
&lt;p&gt;插头dp，专门解决网格连通性问题&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法题单】区间动态规划相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-classification/interval-dp/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-classification/interval-dp/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;区间动态规划问题&lt;/h1&gt;
&lt;p&gt;区间动态规划是一类以区间 $[l,r]$ 作为状态单位的动态规划模型，通常定义 $dp[l][r]$ 表示区间 $[l,r]$ 内某种性质的最优值或可行性结果。这类问题的核心特征在于：状态天然依赖左右端点，转移围绕区间边界关系或区间划分方式展开。与线性 DP 不同，它关注的不是当前位置如何由前一位置转移，而是 &lt;strong&gt;一个区间如何由更小的区间构成&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;从结构角度来看，区间 DP 的转移方式大致可以归纳为两类：&lt;strong&gt;端点拓展型&lt;/strong&gt; 与 &lt;strong&gt;区间分治型&lt;/strong&gt; 。以有效括号为例，合法的有效括号结构既包含嵌套结构，如 &lt;code&gt;&quot;(())&quot;&lt;/code&gt; ，也包含并列结构，如 &lt;code&gt;&quot;()()&quot;&lt;/code&gt; 。这两种合法的有效括号形态，恰好对应区间 DP 的两种典型转移方式。&lt;/p&gt;
&lt;p&gt;端点拓展型强调左右端点之间的匹配关系，其典型形式是当区间两端满足某种条件时，可以由内部区间转移而来：&lt;/p&gt;
&lt;p&gt;$$
dp[l][r] \longleftarrow dp[l+1][r-1]
$$&lt;/p&gt;
&lt;p&gt;这种转移方式体现了 &lt;strong&gt;嵌套结构&lt;/strong&gt; 的演变过程，大区间被视为在小区间的基础上由左右两端向外拓展而成。以有效括号为例，当两端字符配对时，整个区间的性质便完全继承自内部的子区间 $[l+1, r-1]$ 。这种逻辑在回文类问题中同样适用，&lt;strong&gt;回文串的对称性&lt;/strong&gt; 本质上就是一种层层嵌套的结构，两端字符的匹配是大区间能够延续内部区间回文特性的前提。这种模式将复杂区间拆解为一层层的嵌套形态，刻画了状态在空间上的 &lt;strong&gt;层次包含关系&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;另一类是区间分治型转移，其核心思想是通过枚举划分点，将区间拆分为左右两个并列子区间：&lt;/p&gt;
&lt;p&gt;$$
dp[l][r] = \min_{k \in [l,r)} \big( dp[l][k] + dp[k+1][r] \big)
$$&lt;/p&gt;
&lt;p&gt;这种转移方式体现了 &lt;strong&gt;并列结构&lt;/strong&gt; 的演变过程，大区间被视为由两个相邻子区间合并而成。以有效括号为例，枚举划分点 $k$ 的本质是在寻找两个独立的有效括号序列的分界线，从而将大区间拆解为互不重叠的两个子区间。这种模式提供了另一种区间拓展的视角，不再关注 &lt;strong&gt;区间端点的延伸&lt;/strong&gt;，而是更 &lt;strong&gt;侧重于小区间的拼接&lt;/strong&gt;，深刻地揭示了复杂状态是如何由局部基础单元通过 &lt;strong&gt;逻辑整合&lt;/strong&gt; 与 &lt;strong&gt;结构堆叠&lt;/strong&gt; 演化而来的。&lt;/p&gt;
&lt;p&gt;从另一个角度分析，我们所枚举的划分点 $k$ ，也可以看作是当前区间在 “最后一步操作” 或 “第一次操作” 中选定的关键位置。若将 $k$ 视为一棵二叉树的根节点，那么子区间 $[l,k]$ 与 $[k+1,r]$ 便分别对应其左右子树，通过对子区间的持续递归分解，整个决策过程便形成一棵 &lt;strong&gt;二叉分治树&lt;/strong&gt; 。许多经典的区间类问题，其核心都在于通过这种 &lt;strong&gt;选定中心并递归处理两侧&lt;/strong&gt; 的方式，将复杂的整体结构转化为局部子结构的组合。&lt;/p&gt;
&lt;h2&gt;最长回文子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-palindromic-subsequence/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个字符串 &lt;code&gt;s&lt;/code&gt; ，找到它的 &lt;strong&gt;最长回文子序列&lt;/strong&gt; 的长度。子序列是从原字符串中删除部分字符后留下的序列，并 &lt;strong&gt;不改变剩余字符的顺序&lt;/strong&gt; 。回文序列是正读和反读都相同的字符串。&lt;/p&gt;
&lt;p&gt;例如，字符串 &lt;code&gt;&quot;bbbab&quot;&lt;/code&gt; 的最长回文子序列是 &lt;code&gt;&quot;bbbb&quot;&lt;/code&gt; ，长度为 &lt;code&gt;4&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示字符串 &lt;code&gt;s&lt;/code&gt; 的最长回文子序列的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bbbab
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cbbd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;嵌套类型题&lt;/p&gt;
&lt;h2&gt;构造回文字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个字符串 &lt;code&gt;s&lt;/code&gt; ，你可以在 &lt;strong&gt;任意位置插入字符&lt;/strong&gt; 使得这个字符串变成 &lt;strong&gt;回文串&lt;/strong&gt; 。请返回使字符串成为回文串所需要的 &lt;strong&gt;最少插入次数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;注意，插入字符不会删除原有字符，你可以在字符串的头部、尾部或中间任意位置插入字符。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 500$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示将字符串 &lt;code&gt;s&lt;/code&gt; 变成回文串所需的最少插入次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;zzazz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mbadm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;leetcode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;嵌套类型题&lt;/p&gt;
&lt;h2&gt;基于数组的博弈&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/predict-the-winner/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的整数数组 &lt;code&gt;nums&lt;/code&gt; ，两位玩家轮流进行游戏，每一轮玩家从数组的 &lt;strong&gt;最左端或最右端&lt;/strong&gt; 选择一个数字取走，取走后该数字从数组中移除。两位玩家都采用 &lt;strong&gt;最优策略&lt;/strong&gt; 玩游戏。&lt;/p&gt;
&lt;p&gt;玩家 $1$ 先手，玩家 $2$ 后手。游戏结束后，玩家 $1$ 和玩家 $2$ 分别拥有自己的数字总和。请你预测 &lt;strong&gt;先手玩家是否能够获胜&lt;/strong&gt;（即先手玩家的总和大于或等于后手玩家的总和）。&lt;/p&gt;
&lt;p&gt;如果先手玩家有获胜的策略，则返回 &lt;code&gt;true&lt;/code&gt; ，否则返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 20$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^7$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组 $nums$ 的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个布尔值（ &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt; ），表示在双方采用最优策略的情况下，先手玩家是否能够获胜。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 5 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 5 233 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;题目明示端点扩展&lt;/p&gt;
&lt;h2&gt;多边形三角剖分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个 &lt;strong&gt;凸多边形&lt;/strong&gt; 的顶点数 $n$ 和一个长度为 $n$ 的整数数组 &lt;code&gt;values&lt;/code&gt; ，其中 &lt;code&gt;values[i]&lt;/code&gt; 表示第 $i$ 个顶点的权值。&lt;/p&gt;
&lt;p&gt;对于一个凸多边形的三角剖分，每次选取三个顶点组成一条三角形，并将该三角形一分为二直到整个多边形被划分成非重叠三角形。
定义三角剖分的 &lt;strong&gt;得分&lt;/strong&gt; 为所有组成三角形的权值乘积之和，每个三角形的得分为三个顶点对应权值的乘积。&lt;/p&gt;
&lt;p&gt;请返回这个凸多边形所有合法三角剖分中能够得到的 &lt;strong&gt;最低得分&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$3 \leq values.length \leq 50$&lt;/li&gt;
&lt;li&gt;$1 \leq values[i] \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示顶点数（同时也是数组长度）。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $values$ 中每个顶点的权值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$values_1 \quad values_2 \quad \ldots \quad values_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有合法三角剖分中能得到的最低得分。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
3 7 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;144
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;打气球最大得分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/burst-balloons/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有若干个气球排成一排，第 $i$ 个气球的数字为 &lt;code&gt;nums[i]&lt;/code&gt; 。当你戳破一个气球时，你将获得的金币数等于该气球左侧和右侧未被戳破气球的数字乘积再乘以该气球本身。如果某一侧已经没有未戳破的气球，则视为数字 $1$ 。&lt;/p&gt;
&lt;p&gt;换句话说，假设你依次戳破气球的顺序为 $i_1, i_2, \ldots, i_n$，戳破第 $k$ 个气球时获得的金币为：&lt;/p&gt;
&lt;p&gt;$$
coins = nums[i_k] × left × right
$$&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;left&lt;/code&gt; 是第 $i_k$ 个气球左边尚未被戳破的气球数字（若不存在则为 $1$），&lt;code&gt;right&lt;/code&gt; 是第 $i_k$ 个气球右边尚未被戳破的气球数字（若不存在则为 $1$ ）。&lt;/p&gt;
&lt;p&gt;请返回你能获得的 &lt;strong&gt;最大金币数&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 300$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示气球的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $nums$ 中每个气球的数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能获得的最大金币数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
3 1 5 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;167
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
1 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;布尔运算表达式&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/boolean-evaluation-lcci/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个只包含字符 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 、&lt;code&gt;&apos;1&apos;&lt;/code&gt; 和布尔运算符 &lt;code&gt;&apos;&amp;amp;&apos;&lt;/code&gt; 、&lt;code&gt;&apos;|&apos;&lt;/code&gt; 、&lt;code&gt;&apos;^&apos;&lt;/code&gt; 的字符串 &lt;code&gt;s&lt;/code&gt; 以及一个布尔值 &lt;code&gt;result&lt;/code&gt; 。
你需要统计有多少种不同的方式，在运算表达式中加入括号，使得整个表达式的计算结果为 &lt;code&gt;result&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;布尔运算规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;&amp;amp;&apos;&lt;/code&gt; 表示逻辑与，如果左右两边都为真（ &lt;code&gt;1&lt;/code&gt; ）则结果为真，否则为假（ &lt;code&gt;0&lt;/code&gt; ）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;|&apos;&lt;/code&gt; 表示逻辑或，只要左右任意一侧为真（ &lt;code&gt;1&lt;/code&gt; ）则结果为真。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;^&apos;&lt;/code&gt; 表示逻辑异或，当左右两侧不同时结果为真。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要返回使表达式结果为给定 &lt;code&gt;result&lt;/code&gt; 的 &lt;strong&gt;不同括号组合的数量&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 19$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 由数字 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 、&lt;code&gt;&apos;1&apos;&lt;/code&gt; 与运算符 &lt;code&gt;&apos;&amp;amp;&apos;&lt;/code&gt; 、&lt;code&gt;&apos;|&apos;&lt;/code&gt; 、&lt;code&gt;&apos;^&apos;&lt;/code&gt; 交替组成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result&lt;/code&gt; 为布尔值（ &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt; ）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 &lt;code&gt;s&lt;/code&gt; ，表示布尔运算表达式。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $result$ ，若为 &lt;code&gt;1&lt;/code&gt; 表示目标结果为 &lt;code&gt;true&lt;/code&gt; ，若为 &lt;code&gt;0&lt;/code&gt; 表示目标结果为 &lt;code&gt;false&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$result$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有不同的括号插入方式中，能够让表达式计算结果等于 $result$ 的方案数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1^0|0|1
0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0&amp;amp;0&amp;amp;0&amp;amp;1^1|0
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;嵌套类型题&lt;/p&gt;
&lt;h2&gt;最高加分二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1040&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;有效括号字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/e391767d80d942d29e6095a935a5b96b&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个由 &lt;code&gt;&apos;[&apos;&lt;/code&gt; 、&lt;code&gt;&apos;]&apos;&lt;/code&gt; 、&lt;code&gt;&apos;(&apos;&lt;/code&gt; 、&lt;code&gt;&apos;)&apos;&lt;/code&gt; 组成的字符串，请问最少插入多少个括号才能使这个字符串的所有括号 &lt;strong&gt;左右配对&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;例如，当前字符串是 &lt;code&gt;&quot;([[])&quot;&lt;/code&gt; ，那么插入一个 &lt;code&gt;&apos;]&apos;&lt;/code&gt; 即可满足所有括号匹配的要求。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;字符串长度满足 $1 \leq n \leq 100$&lt;/li&gt;
&lt;li&gt;字符串仅由上述 $4$ 种括号字符组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示使字符串所有括号配对所需插入的最少括号数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;([[])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;([])[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;奇怪的打印机器&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/strange-printer/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有台奇怪的打印机，它每次可以打印一串 &lt;strong&gt;相同字符&lt;/strong&gt; 的序列，也就是说每次打印过程只能选择一个字符并将其连续地打印在一段区间上。&lt;/p&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;s&lt;/code&gt; ，奇怪的打印机最开始是空的，它可以在任意位置开始打印。每次打印它会选择一个区间并将同一个字符覆盖写入，在这个过程中会覆盖掉原来存在的字符。&lt;/p&gt;
&lt;p&gt;请问这台打印机 &lt;strong&gt;打印出字符串 &lt;code&gt;s&lt;/code&gt; 最少需要多少次打印操作&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 100$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示奇怪的打印机打印出字符串 &lt;code&gt;s&lt;/code&gt; 最少需要的操作次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aba
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaabbb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;尝试找到题目中的嵌套、并列关系&lt;/p&gt;
&lt;h2&gt;合唱班站队问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3205&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;为了在晚会上有更好的演出效果，小 A 需要按照身高将合唱队的人排成一个队形。合唱队共有 $n$ 个人，编号从 $1$ 到 $n$ ，第 $i$ 个人的身高为 $h_i$ ，且所有身高互不相同。&lt;/p&gt;
&lt;p&gt;小 A 想知道有多少种 &lt;strong&gt;初始队形排列&lt;/strong&gt; 能够最终生成他理想中的队形。生成过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，所有人按某种顺序站成一个初始队形；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后从左到右依次将每个人插入新的队列中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个人直接进入空队列；&lt;/li&gt;
&lt;li&gt;对于后面的每个人，如果他比队列中最后一个人高，则将他插入队列的 &lt;strong&gt;最右边&lt;/strong&gt; ；&lt;/li&gt;
&lt;li&gt;如果他比队列中最后一个人矮，则将他插入队列的 &lt;strong&gt;最左边&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当所有人都插入结束后，就得到一个最终队形。如果最终队形恰好与给定的理想队形一致，则认为这个初始队形是合法的。&lt;/p&gt;
&lt;p&gt;请你计算 &lt;strong&gt;所有能生成理想队形的初始队形数量&lt;/strong&gt; ，并输出答案对 $19650827$ 取模后的结果。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 1000$&lt;/li&gt;
&lt;li&gt;$1000 \leq h_i \leq 2000$&lt;/li&gt;
&lt;li&gt;所有身高互不相同&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示队伍中人的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示理想队形中每个人的身高。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$h_1 \quad h_2 \quad \ldots \quad h_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的合法初始队形数量对 $19650827$ 取模后的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1701 1702 1703 1704
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;题目明示端点扩展&lt;/p&gt;
&lt;h2&gt;移除盒子的得分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-boxes/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个由数字表示的盒子序列 &lt;code&gt;boxes&lt;/code&gt; ，每个盒子的颜色由一个正整数表示。你可以执行以下操作：&lt;/p&gt;
&lt;p&gt;每一次操作，你可以选择 &lt;strong&gt;连续的一组颜色相同的盒子&lt;/strong&gt;（长度至少为 $1$ ），将这一组盒子取出并获得积分。取出长度为 $k$ 的这一组盒子后，你将获得的积分为 $k^2$ 。&lt;/p&gt;
&lt;p&gt;取出之后，剩余的盒子会重新拼接成一个新的连续序列。你可以重复执行上述操作，直到所有盒子都被取出。&lt;/p&gt;
&lt;p&gt;请返回你能获得的 &lt;strong&gt;最大积分数&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq boxes.length \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq boxes[i] \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示盒子数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $boxes$ 中每个盒子的颜色。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$boxes_1 \quad boxes_2 \quad \ldots \quad boxes_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能够获得的最大积分数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
1 3 2 2 2 3 4 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;23
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;尝试找到题目中的嵌套、并列关系&lt;/p&gt;
&lt;h2&gt;不同的回文序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-different-palindromic-subsequences/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个字符串 &lt;code&gt;s&lt;/code&gt; ，请你统计其中 &lt;strong&gt;不同的非空回文子序列&lt;/strong&gt; 的个数。&lt;/p&gt;
&lt;p&gt;需要注意以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子序列定义为可以通过删除原字符串中任意位置的字符（可能不连续）得到的新字符串；&lt;/li&gt;
&lt;li&gt;只统计 &lt;strong&gt;不同的回文子序列&lt;/strong&gt; ，重复的回文序列只计数一次；&lt;/li&gt;
&lt;li&gt;最终结果可能很大，请返回答案对 $10^9 + 7$ 取模的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示字符串 &lt;code&gt;s&lt;/code&gt; 中不同的非空回文子序列的数量，对 $10^9 + 7$ 取模后的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bccb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;104860361
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;嵌套类型题&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法题单】背包动态规划相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-classification/knapsack-dp/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-classification/knapsack-dp/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;第一类背包问题&lt;/h1&gt;
&lt;p&gt;在动态规划中，常见的问题形态大致可以划分为两类：一类是 &lt;strong&gt;子序列类型问题&lt;/strong&gt; ，另一类是 &lt;strong&gt;子数组类型问题&lt;/strong&gt; 。二者的核心差异在于选取结构的不同。子数组问题强调的是 &lt;strong&gt;连续性约束&lt;/strong&gt; ，所选择的元素必须在原序列中连续出现，其状态往往围绕区间端点展开；而子序列问题则强调的是 &lt;strong&gt;选择与不选择的组合结构&lt;/strong&gt; ，元素之间不要求连续，只需满足给定约束条件即可。&lt;/p&gt;
&lt;p&gt;从这一角度出发可以看到，&lt;strong&gt;背包问题本质上属于子序列类型问题&lt;/strong&gt; 。在背包模型中，我们面对的是若干独立的物品，每件物品要么被选取，要么不被选取。最终得到的解，实质上就是所有物品集合的一个子集，或者更抽象地说，是原物品序列的一个子序列。问题的关键并不在于物品的相对位置关系，而在于每件物品是否被纳入解集中，以及这些选择在容量约束下是否可行。&lt;/p&gt;
&lt;p&gt;从更抽象的层面看，背包问题可以理解为在 “物品维度” 上进行决策，在 “容量维度” 上进行约束控制。决策的本质是对子序列的构造，而容量只是一个全局约束变量。这种结构决定了背包问题天然适合采用逐物品推进的动态规划框架，并体现出典型的子序列型 DP 的特征。&lt;/p&gt;
&lt;h3&gt;完全背包问题&lt;/h3&gt;
&lt;p&gt;对于完全背包而言，由于同一种物品可以被选择任意多次，其状态转移在形式上与 01 背包有所不同，但从问题结构的角度分析，它仍然属于典型的子序列类型问题。&lt;/p&gt;
&lt;p&gt;在 01 背包中，每种物品至多出现一次，因此构造的是一个 &lt;strong&gt;不含重复元素的子序列&lt;/strong&gt; ；而在完全背包中，每种物品可以被重复选取，本质上是在构造一个 &lt;strong&gt;允许重复元素的子序列&lt;/strong&gt; 。换言之，问题依然是在所有物品构成的序列上进行选择，只是允许对某个位置进行多次取用。&lt;/p&gt;
&lt;p&gt;在状态设计上，仍定义 $dp[i][j]$ 表示在前 $i$ 种物品中，容量不超过 $j$ 时的最大价值。此时转移方程为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max \big( dp[i-1][j], , dp[i][j - v_i] + w_i \big)
$$&lt;/p&gt;
&lt;p&gt;与 01 背包的区别在于，第二项使用的是 $dp[i][\cdot]$ 而非 $dp[i-1][\cdot]$ 。这意味着在处理第 $i$ 种物品时，可以在当前层继续使用该物品，从而实现 “无限次选取” 的效果。&lt;/p&gt;
&lt;h3&gt;多重背包问题&lt;/h3&gt;
&lt;p&gt;多重背包处于 01 背包与完全背包之间，是对两者在选取次数约束上的折中形式。相较于 01 背包中 “每种物品至多选一次” 的严格限制，以及完全背包中 “每种物品可以无限选取” 的放宽条件，多重背包为每种物品设定了一个 &lt;strong&gt;有限的可选次数上界&lt;/strong&gt; 。因此在建模与转移过程中，既不能简单地按一次性决策处理，也不能直接允许无限叠加，而必须显式刻画并控制选取次数的范围。&lt;/p&gt;
&lt;p&gt;设第 $i$ 种物品体积为 $v_i$ ，价值为 $w_i$ ，最多可选 $s_i$ 次。仍定义二维状态 $dp[i][j]$ 表示在前 $i$ 种物品中，容量不超过 $j$ 时的最大价值。此时转移方程为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max_{0 \leq k \leq s_i} \big( dp[i-1][j - k v_i] + k w_i \big)
$$&lt;/p&gt;
&lt;p&gt;这个转移方程清晰地刻画了多重背包的结构：在子序列型 DP 的基础上，进一步枚举选取的次数。可以看到，当 $s_i = 1$ 时退化为 01 背包；当 $s_i \to \infty$ 时转化为完全背包。因此，多重背包是对前两类问题的自然推广。&lt;/p&gt;
&lt;p&gt;然而，若直接对 $k$ 进行枚举，时间复杂度将达到 $O(n V s_i)$ ，在 $s_i$ 较大时难以接受。为了优化这一枚举过程，可以从转移结构入手进行重组。将容量按模 $v_i$ 的余数进行分组：&lt;/p&gt;
&lt;p&gt;$$
j \equiv r \pmod{v_i}
$$&lt;/p&gt;
&lt;p&gt;在同一余数组内，容量可以表示为：&lt;/p&gt;
&lt;p&gt;$$
j = r + t v_i
$$&lt;/p&gt;
&lt;p&gt;在该表示下，原转移式可重新整理为关于 $t$ 的滑动窗口形式，从而将问题转化为在一个线性序列上求带长度限制的最大值问题。整理后可以发现，其本质正是一个 &lt;strong&gt;滑动窗口最大值问题&lt;/strong&gt; 。因此，可以使用单调队列维护窗口内的最优候选值，在每个容量位置实现 $O(1)$ 转移，从而将整体复杂度优化到 $O(nV)$ 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;多重背包单调队列优化的详细讲解可以看下面这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=578590023&amp;amp;bvid=BV1Nz4y1c71M&amp;amp;cid=1589053836&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;多重背包更加常用的优化方法是&lt;a href=&quot;#%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%88%86%E7%BB%84%E4%BC%98%E5%8C%96&quot;&gt;二进制分组优化&lt;/a&gt;（或称二进制拆分）。其核心思想在于对 “选取次数上界” 进行结构化重组，将原本需要显式处理的次数限制转化为若干个更简单的决策单位，从而避免在状态转移中直接枚举选取次数。这种方法并不改变问题本质，而是通过对物品结构的等价变换，使问题重新回归到更成熟、实现更稳定的动态规划框架中，因此在实际应用中往往更加常见且易于实现。&lt;/p&gt;
&lt;h2&gt;完全平方数构造&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/perfect-squares/description/?source=vscode&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;构造目标正整数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1128&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;二进制与字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/ones-and-zeroes/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个字符串数组 &lt;code&gt;strs&lt;/code&gt; ，其中每个字符串只包含 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 。同时给定两个整数 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;要求从 &lt;code&gt;strs&lt;/code&gt; 中选出最多 &lt;strong&gt;多少个字符串&lt;/strong&gt; ，使得选出的字符串中 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 的总数不超过 &lt;code&gt;m&lt;/code&gt; ，&lt;code&gt;&apos;1&apos;&lt;/code&gt; 的总数不超过 &lt;code&gt;n&lt;/code&gt; 。每个字符串最多只能使用一次。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq strs.length \leq 600$&lt;/li&gt;
&lt;li&gt;$1 \leq strs[i].length \leq 100$&lt;/li&gt;
&lt;li&gt;$strs[i]$ 仅包含 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;1&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;$1 \leq m, n \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$m$ 和 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个只包含 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad m \quad n$&lt;/p&gt;
&lt;p&gt;$strs_1 \quad strs_2 \quad \ldots \quad strs_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在满足 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 数量不超过 $m$ 且 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 数量不超过 $n$ 的前提下，最多能选出的字符串数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 5 3
10 0001 111001 1 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 1 1
10 0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;罪犯的盈利计划&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/profitable-schemes/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数 &lt;code&gt;n&lt;/code&gt; 表示成员总数，以及两个整数数组 &lt;code&gt;group&lt;/code&gt; 和 &lt;code&gt;profit&lt;/code&gt; 。数组中第 $i$ 个元素表示第 $i$ 种犯罪所需要的成员数和能带来的利润。若一个成员参与了一种犯罪，则该成员不能再参与其它犯罪。&lt;/p&gt;
&lt;p&gt;我们称一个犯罪计划为 &lt;strong&gt;盈利计划&lt;/strong&gt; ，当且仅当该计划中涉及的犯罪总利润不小于 &lt;code&gt;minProfit&lt;/code&gt; ，并且参与犯罪的成员总数不超过 &lt;code&gt;n&lt;/code&gt; 。请统计 &lt;strong&gt;盈利计划的总数&lt;/strong&gt; 。由于结果可能很大，请返回答案对 $10^9 + 7$ 取模的值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq minProfit \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq group.length \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq group[i] \leq 100$&lt;/li&gt;
&lt;li&gt;$profit.length == group.length$&lt;/li&gt;
&lt;li&gt;$0 \leq profit[i] \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$minProfit$ 和 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $group$ 中每种犯罪所需的成员数。&lt;/li&gt;
&lt;li&gt;第三行包含 $N$ 个整数，表示数组 $profit$ 中每种犯罪能带来的利润。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad n \quad minProfit$&lt;/p&gt;
&lt;p&gt;$group_1 \quad group_2 \quad \ldots \quad group_N$&lt;/p&gt;
&lt;p&gt;$profit_1 \quad profit_2 \quad \ldots \quad profit_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有满足条件的盈利计划的数量，对 $10^9 + 7$ 取模后的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 5 3
2 2
2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 10 5
2 3 5
6 7 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;买干草最小花销&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2918&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;约翰的干草库存已经告罄，他打算为奶牛们采购干草。现在已知有 $N$ 个干草公司，编号从 $1$ 到 $N$ 。第 $i$ 家公司卖的干草包重量为 $P_i$ 磅，需要开销 $C_i$ 美元，并且每家公司都有 &lt;strong&gt;充足货源&lt;/strong&gt; ，可以卖出 &lt;strong&gt;无限多包干草&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;约翰希望至少采购到 $H$ 磅干草，请帮助他找到满足这一需求的 &lt;strong&gt;最低总开销&lt;/strong&gt; 。也就是说，在购买总重量达到或超过 $H$ 的前提下，求最小的花费总和。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq H \leq 50000$&lt;/li&gt;
&lt;li&gt;$1 \leq P_i \leq 5000$&lt;/li&gt;
&lt;li&gt;$1 \leq C_i \leq 5000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $H$ ，分别表示干草公司数量和至少需要采购的干草磅数。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行包含两个整数 $P_i$ 和 $C_i$ ，分别表示第 $i$ 家公司的干草包重量和价格。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad H$&lt;/p&gt;
&lt;p&gt;$P_1 \quad C_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$P_N \quad C_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示采购到至少 $H$ 磅干草所需的最少花费。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 15 
3 2 
5 3 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;“至少、恰好、不超过” 三种尝试方法，分析它们之间的优劣&lt;/p&gt;
&lt;h2&gt;夏季游戏大特惠&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/tJau2o/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;某公司游戏平台的夏季特惠开始了，你决定入手一些游戏。现在你一共有一个预算 &lt;code&gt;X&lt;/code&gt; 元，平台上有 &lt;code&gt;n&lt;/code&gt; 个游戏，每个游戏都有原价和现价，并且购买该游戏可以获得一定的快乐值。&lt;/p&gt;
&lt;p&gt;对于编号为 &lt;code&gt;i&lt;/code&gt; 的游戏，原价为 &lt;code&gt;a_i&lt;/code&gt; 元，现价只要 &lt;code&gt;b_i&lt;/code&gt; 元，那么购买该游戏能 &lt;strong&gt;优惠&lt;/strong&gt; &lt;code&gt;a_i - b_i&lt;/code&gt; 元，并带来快乐值 &lt;code&gt;w_i&lt;/code&gt; 。由于优惠的存在，你可能会出现 “冲动消费” 使得总支出超过预算，只要你的 &lt;strong&gt;总优惠金额不小于超过预算的金额&lt;/strong&gt; ，心理上就不会觉得吃亏。&lt;/p&gt;
&lt;p&gt;你的目标是在这种 “心理上不吃亏” 的前提下，选择一些游戏使得 &lt;strong&gt;总快乐值最大&lt;/strong&gt; 。每个游戏最多只能购买一次。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 500$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i, b_i \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq w_i \leq 10^5$&lt;/li&gt;
&lt;li&gt;总预算 $X$ 是整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含四行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $X$ ，分别表示游戏数量和预算。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，分别表示每个游戏的原价数组 $a$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，分别表示每个游戏的现价数组 $b$ 。&lt;/li&gt;
&lt;li&gt;第四行包含 $n$ 个整数，分别表示每个游戏的快乐值数组 $w$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad X$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_n$&lt;/p&gt;
&lt;p&gt;$w_1 \quad w_2 \quad \ldots \quad w_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在满足总优惠金额不小于超过预算金额的前提下，你能获得的最大快乐值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 100
100 73 60
100 89 35
30 21 30
10 8 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;100
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 100
100 100 60
80 80 35
21 21 30
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;60
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;构造目标和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/target-sum&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;target&lt;/code&gt; 。你可以对数组中的每个元素选择 &lt;strong&gt;加号（+）或减号（−）&lt;/strong&gt; ，从而构造一个表达式。例如：对于数组 &lt;code&gt;[1, 1, 1, 1, 1]&lt;/code&gt; ，可以有如下表达式：&lt;/p&gt;
&lt;p&gt;$$
-1 + 1 + 1 + 1 + 1 = 3
$$&lt;/p&gt;
&lt;p&gt;请找出并返回可以使最终表达式等于 &lt;code&gt;target&lt;/code&gt; 的 &lt;strong&gt;所有不同表达式的方案数&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 20$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 1000$&lt;/li&gt;
&lt;li&gt;$-1000 \leq target \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个个整数 $N$ 和 $target$ ，$N$ 表示数组 $nums$ 的长度，$target$ 表示目标和。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $nums$ 的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad target$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有能使表达式结果等于 $target$ 的不同方案数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 3
1 1 1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 1
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最后的石头重量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/last-stone-weight-ii&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有一堆石头，每块石头的重量都是正整数。每一回合，从中选出 &lt;strong&gt;两块石头&lt;/strong&gt; ，然后将它们一起碰撞：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设这两块石头的重量分别为 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; ，且 &lt;code&gt;x &amp;lt;= y&lt;/code&gt; ；&lt;/li&gt;
&lt;li&gt;碰撞后，如果 &lt;code&gt;x == y&lt;/code&gt; ，则两块石头都会 &lt;strong&gt;完全粉碎&lt;/strong&gt; 丢弃；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;x != y&lt;/code&gt; ，则那块较轻的石头 &lt;code&gt;x&lt;/code&gt; 会完全粉碎，而较重的那块石头会剩下重量为 &lt;code&gt;y - x&lt;/code&gt; 的碎石重新放回石头堆中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终最多只会剩下一块石头。请返回 &lt;strong&gt;剩下那块石头的最小可能重量&lt;/strong&gt;（如果没有剩下则为 &lt;code&gt;0&lt;/code&gt; ）。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq stones.length \leq 30$&lt;/li&gt;
&lt;li&gt;$1 \leq stones[i] \leq 100$&lt;/li&gt;
&lt;li&gt;石头重量都是正整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示石头数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $stones$ 中每块石头的重量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$stones_1 \quad stones_2 \quad \ldots \quad stones_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最后剩下的石头的最小可能重量（若没有石头剩下则输出 &lt;code&gt;0&lt;/code&gt; ）。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
2 7 4 1 8 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
31 26 33 21 40
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;第二类背包问题&lt;/h1&gt;
&lt;p&gt;分组背包是一类具有组内互斥结构的特殊背包问题，本质上可以理解为一种 &lt;strong&gt;按组进行决策的背包模型&lt;/strong&gt; 。与普通背包中 “每件物品彼此独立” 不同，分组背包会先将所有候选物品划分为若干组。在同一组内，各个选项之间是 &lt;strong&gt;互斥关系&lt;/strong&gt; ，而不同组之间则可以同时选择，但所有选择共同受到一个 &lt;strong&gt;统一的容量约束&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在状态设计上，仍可定义二维状态 $dp[i][j]$ 表示在前 $i$ 组中进行选择且容量不超过 $j$ 时所能获得的最大价值。设第 $i$ 组共有若干个物品，第 $k$ 个物品的体积为 $v_{i,k}$ ，价值为 $w_{i,k}$ 。则转移方程为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max \Big( dp[i-1][j], , \max_k { dp[i-1][j - v_{i,k}] + w_{i,k} } \Big)
$$&lt;/p&gt;
&lt;p&gt;其中第一项表示 “第 $i$ 组不选任何物品” ，第二项表示 “第 $i$ 组选择某一个物品” 。可以看到，状态始终从 $i-1$ 组转移而来，正是为了保证同一组内不会选取多个物品，从而满足组内互斥的约束。&lt;/p&gt;
&lt;p&gt;除了普通背包与分组背包之外，还有一类称为 &lt;strong&gt;依赖背包问题&lt;/strong&gt; 的重要模型。所谓的依赖背包，是指物品之间存在 &lt;strong&gt;选择前提关系&lt;/strong&gt; 。不同于普通背包中各物品的彼此独立，在依赖背包中，某些物品只有在其前置物品被选取的前提下才能被选择。这类关系通常可以抽象为一棵有根树结构：若选择某个节点，则必须同时选择其所有祖先节点；若父节点未被选取，则其子节点必然不可选。换言之，依赖背包的本质在于引入了一种 &lt;strong&gt;自上而下的结构性约束&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;依赖条件打破了普通 01 背包中 &lt;strong&gt;物品独立决策&lt;/strong&gt; 的基本假设，使得简单的逐物品转移不再适用。某一物品是否可以参与状态更新，不仅取决于当前剩余容量，还取决于其父节点是否已经被纳入当前方案。因此，问题的核心不再只是容量分配，而是 &lt;strong&gt;如何在满足结构约束的前提下完成合法选择&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;从理论角度看，这类问题也可以借助分组思想进行刻画。若将依赖关系组织为一棵依赖树，则可以枚举该树中所有满足依赖约束的合法选取子结构，并将每一种合法结构视为一个组内候选项，从而转化为分组背包模型。然而，这种方法本质上是在对整棵依赖树进行 &lt;strong&gt;状态展开&lt;/strong&gt; ，其合法组合数量通常会随着树规模迅速增长，时间复杂度难以控制。因此，这种转化方式仅在 &lt;strong&gt;依赖规模极小&lt;/strong&gt; 的情况下才具有可行性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体的实现过程可以看下面这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=97870670&amp;amp;bvid=BV1JE411A75B&amp;amp;cid=167073202&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;当依赖树规模较大时，更为自然且高效的处理方式是直接在树结构上进行动态规划，即采用&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/tree-dp/#%E6%A0%91%E5%9E%8B%E4%BE%9D%E8%B5%96%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98&quot;&gt;树形动态规划&lt;/a&gt;的方法。通过在每个节点处维护 “在其子树内选取若干体积时的最优价值” ，并自底向上逐步合并子树状态，可以在不显式枚举所有合法组合的前提下完成整体决策。&lt;/p&gt;
&lt;h3&gt;二进制分组优化&lt;/h3&gt;
&lt;p&gt;在多重背包问题中，更为常见且实用的优化方式是 &lt;strong&gt;二进制分组优化&lt;/strong&gt; 。其核心思想是：将 “某种物品最多可选 $s_i$ 次” 的数量限制，通过二进制拆分转化为若干个 “至多选一次” 的新物品，从而把多重背包等价地转化为一个 01 背包问题。具体来说，对于某种物品，若其最多可选 $s_i$ 次，可以将 $s_i$ 按二进制形式拆分为若干块，例如：&lt;/p&gt;
&lt;p&gt;$$
s_i = 1 + 2 + 4 + \cdots + 2^k + r
$$&lt;/p&gt;
&lt;p&gt;据此，可以将原物品拆分为若干个体积与价值按倍数放大的新物品：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;体积为 $1 \cdot v_i$ ，价值为 $1 \cdot w_i$&lt;/li&gt;
&lt;li&gt;体积为 $2 \cdot v_i$ ，价值为 $2 \cdot w_i$&lt;/li&gt;
&lt;li&gt;体积为 $4 \cdot v_i$ ，价值为 $4 \cdot w_i$&lt;/li&gt;
&lt;li&gt;$\ldots$&lt;/li&gt;
&lt;li&gt;体积为 $r \cdot v_i$ ，价值为 $r \cdot w_i$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个拆分后的物品都只能选取一次。由于任意一个不超过 $s_i$ 的整数，都可以由这些二进制块唯一表示，因此这种拆分在可行性与最优性上与原问题完全等价。&lt;/p&gt;
&lt;p&gt;从更本质的角度看，我们之所以这样拆分，是为了用 &lt;strong&gt;二进制编码覆盖从 $0$ 到 $s_i$ 的所有取值&lt;/strong&gt; 。任何一个选取次数 $k \le s_i$ ，都可以表示为若干个二进制位之和，而二进制表示的关键特性在于：各个二进制位彼此独立，每一位只有 “取或不取” 两种状态。在二进制视角下，一个次数 $k$ 的形成，其实只是若干个二进制块被选中的结果。因此，我们无需在外层枚举完整的二进制数，而是把每一位是否为 $1$ 的决策，直接融入到背包的状态转移之中。动态规划在进行 $01$ 决策时，会自动完成这些块的组合，从而自然枚举出所有合法次数。&lt;/p&gt;
&lt;p&gt;也正因为 &lt;strong&gt;二进制位之间相互独立&lt;/strong&gt; ，而背包问题本身又具有 “选或不选” 的结构，两者在形式上天然对应，才能通过这种结构重组，把原本对次数的显式枚举，转化为若干次简单的 $01$ 决策。经过拆分后，原问题转化为一个规模略有扩展的 $01$ 背包问题，物品总数由 $n$ 增加到约 $n \log s_i$ 的数量级。这样，原本需要显式枚举次数、时间复杂度为 $O(n V s_i)$ 的做法，就可以优化为 $O\left(n V \log s_i\right)$ 。相比单调队列优化，二进制分组的思路更加直观，代码结构也更贴近标准 01 背包模板，因此在竞赛与实际应用中更加常见。&lt;/p&gt;
&lt;h2&gt;硬币最大面值和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-value-of-k-coins-from-piles&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个 &lt;code&gt;piles&lt;/code&gt; 数组，其中 &lt;code&gt;piles[i]&lt;/code&gt; 是一个整数数组，表示第 &lt;code&gt;i&lt;/code&gt; 堆硬币从上到下排列的硬币价值。&lt;/p&gt;
&lt;p&gt;每次行动，你可以从任意一堆中 &lt;strong&gt;移除最上面的硬币&lt;/strong&gt; 并拾取它的价值。你总共可以执行 &lt;strong&gt;最多 k 次操作&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;请返回在最优策略下你能得到的 &lt;strong&gt;最大硬币总价值&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq piles.length \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq piles[i].length \leq 2000$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 2000$&lt;/li&gt;
&lt;li&gt;每个硬币的价值都是非负整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$，分别表示堆数和最多可取的硬币数量。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行以一个整数 $len_i$ 开头表示硬币的数量，后接 $len_i$ 个整数表示这堆硬币的价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$len_1 \quad piles[1][0] \quad piles[1][1] \quad \ldots \quad piles[1][len_1-1]$&lt;/p&gt;
&lt;p&gt;$len_2 \quad piles[2][0] \quad piles[2][1] \quad \ldots \quad piles[2][len_2-1]$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$len_N \quad piles[N][0] \quad piles[N][1] \quad \ldots \quad piles[N][len_N-1]$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在最多取出 &lt;code&gt;k&lt;/code&gt; 个硬币的前提下，你能获得的最大总价值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
3 1 100 3
3 7 8 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;101
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 7
1 100
1 100
1 100
1 100
1 100
1 100
7 1 1 1 1 1 1 700
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;760
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;樱花的美学价值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1833&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;爱与愁大神后院里种了 $n$ 棵樱花树，每棵树都有一个美学值 $C_i$ 。每天上学前，他都会到后院赏花。每棵樱花树都有一个看花所需的时间 $T_i$ ，看一次可以获得 $C_i$ 的美学值。对于第 $i$ 棵树：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $P_i = 0$ ，表示可以 &lt;strong&gt;无限次&lt;/strong&gt; 观看该树；&lt;/li&gt;
&lt;li&gt;如果 $P_i &amp;gt; 0$ ，表示最多可以观看该树 $P_i$ 次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;爱与愁大神离开去上学前的时间为 $T_s$ ，上学开始的时间为 $T_e$ 。如果他在赏花过程中花费的总时间不超过 $T_e - T_s$ 分钟，就可以按时或提前上学。请你帮助他选择应观看的樱花树组合，使得 &lt;strong&gt;总美学值最大&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10000$&lt;/li&gt;
&lt;li&gt;$T_e - T_s \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq T_i \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq C_i \leq 200$&lt;/li&gt;
&lt;li&gt;$0 \leq P_i \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个时间点 $T_s$ 和 $T_e$ 以及整数 $n$ ，其中时间格式为 &lt;code&gt;hh:mm&lt;/code&gt; ，保证在同一天内且 $T_e \ge T_s$ 。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行包含三个整数 $T_i$ 、$C_i$ 和 $P_i$ ，分别表示樱花树的赏花时间、美学值以及最多观看次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T_s \quad T_e \quad n$&lt;/p&gt;
&lt;p&gt;$T_1 \quad C_1 \quad P_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$T_n \quad C_n \quad P_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在不超过允许时间的情况下所能获得的最大美学值。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6:50 7:00 3
2 1 0
3 3 1
4 5 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tianyicui/pack/blob/master/V2.pdf&quot;&gt;【崔添翼】背包动态规划九讲&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】树型动态规划相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-classification/tree-dp/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-classification/tree-dp/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树型动态规划问题&lt;/h1&gt;
&lt;p&gt;树型动态规划中最核心、也是最基础的操作，是 &lt;strong&gt;子树合并&lt;/strong&gt; 。在树结构中，每个节点的子树之间天然相互独立，只通过父节点发生关联。因此，许多看似复杂的树上问题，都可以被分解为：&lt;strong&gt;先分别求出各个子树的最优解，再由父节点进行合并&lt;/strong&gt; 。这一过程本质上是一种结构化的动态规划，自底向上地逐层汇总信息。&lt;/p&gt;
&lt;p&gt;从这一视角出发，树型 DP 的状态往往定义在 “以某个节点为根的子树” 上，而转移则体现为 &lt;strong&gt;父节点如何整合来自子节点的贡献&lt;/strong&gt; 。只要问题的约束不会在不同子树之间产生耦合关系，子树合并就可以作为基本框架反复使用。&lt;/p&gt;
&lt;p&gt;基于子树合并，不同类型的问题会呈现出不同的转移形式：有的只需要对子节点状态进行简单求和或取最大值，有的需要在父节点处进行背包式的容量分配，还有的则可以借助 DFS 序，将这种合并过程进一步线性化。后文将从若干典型模型出发，逐步展开这些转移方式。&lt;/p&gt;
&lt;h2&gt;树上路径问题&lt;/h2&gt;
&lt;p&gt;由于树是无环连通图，任意两个节点之间存在且仅存在一条简单路径。因此，在树上讨论路径时，不会出现折返或重复经过同一节点的情况，每个节点在一条简单路径中至多出现一次。这种结构特性使得路径问题具有良好的分解性质，可以通过&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/enumeration/#%E5%AF%B9%E8%B1%A1%E4%BA%A4%E6%8D%A2%E8%B4%A1%E7%8C%AE%E6%B3%95&quot;&gt;对象贡献法&lt;/a&gt;进行分析。&lt;/p&gt;
&lt;p&gt;所谓对象贡献法，即避开对 $O(n^2)$ 条路径的直接枚举，转而从局部元素（节点或边）的视角出发，计算其在所有合法路径中的参与度。与其统计每条路径包含哪些节点，不如统计每个节点被多少条路径覆盖。这种视角切换的核心在于：将 “路径对权值的累加” 转化为 “元素对路径的贡献” ，从而利用树上路径的唯一确定性，将复杂的全局统计解构为离散的局部计数问题。&lt;/p&gt;
&lt;p&gt;以节点贡献为例，考虑删除某个节点 $u$ 后，整棵树会被分成若干个连通块，其规模分别为 $s_1, s_2, \dots, s_k$ 。设整棵树共有 $n$ 个节点。则所有经过节点 $u$ 的路径，可以理解为路径的两个端点分别位于不同的连通块（或其中一个端点就是 $u$ 本身）。通过对子树规模进行组合计数，可以计算出经过节点 $u$ 的路径条数，从而得到该点的贡献。&lt;/p&gt;
&lt;p&gt;若问题涉及路径权值之和或路径长度统计，也可以采用类似思路。通常通过一次 DFS 预处理出每个子树的规模或其他辅助信息，再在回溯阶段计算每个节点或每条边在全局路径集合中的出现次数。这样便可将原本看似需要枚举 $O(n^2)$ 条路径的问题，转化为对每个节点或边进行一次局部计算，整体复杂度降为 $O(n)$ 。&lt;/p&gt;
&lt;p&gt;从结构角度看，树上路径问题之所以适合使用贡献法，根本原因在于 &lt;strong&gt;路径的唯一性与无环性&lt;/strong&gt; 。唯一性保证了路径与节点（或边）之间的对应关系是确定的，无环性保证了不会出现复杂的相互依赖关系。因此，全局路径统计问题可以分解为若干个局部贡献的求和问题。&lt;/p&gt;
&lt;h2&gt;二叉树直径长度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/diameter-of-binary-tree/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;树上最大累加和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc416/tasks/abc416_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;邻点决策问题&lt;/h2&gt;
&lt;p&gt;在树结构中，图的边仅存在于父子节点之间，且整体无环。因此，若问题对相邻节点施加约束，则该约束仅作用于父子节点之间，不会在同层节点之间产生横向影响，也不会因环结构而形成复杂的连锁依赖。这种结构性特征保证了约束传播的局部性，使得问题可以通过树形动态规划进行有效分解。&lt;strong&gt;树的无环性是约束可分解性的前提&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;考虑一类典型模型：要求在树上选择若干节点，使得任意一条边的两个端点不能同时被选。设节点 $u$ 的权值为 $w_u$ ，其子节点集合为 ${v_1, v_2, \ldots, v_m}$ 。定义状态 $dp[u][0]$ 表示在以 $u$ 为根的子树中，且不选择节点 $u$ 时所能获得的最大权值；定义状态 $dp[u][1]$ 表示在以 $u$ 为根的子树中，且选择节点 $u$ 时所能获得的最大权值。&lt;/p&gt;
&lt;p&gt;由于约束仅存在于父子之间，状态转移可直接由局部关系确定。&lt;strong&gt;全局最优性可以通过对子树最优性的组合得到，而不需要额外的全局协调&lt;/strong&gt; 。当选择节点 $u$ 时，根据相邻点不可同时选的约束，其所有子节点均不能被选，因此有：&lt;/p&gt;
&lt;p&gt;$$
dp[u][1] = w_u + \sum_{i=1}^{m} dp[v_i][0]
$$&lt;/p&gt;
&lt;p&gt;当不选择节点 $u$ 时，对子节点不再施加限制，每个子节点可以独立地在 “选与不选” 两种状态中取最优，因此有：&lt;/p&gt;
&lt;p&gt;$$
dp[u][0] = \sum_{i=1}^{m} \max \Big( dp[v_i][0],\ dp[v_i][1] \Big)
$$&lt;/p&gt;
&lt;p&gt;上述转移仅依赖于子节点的状态，且不存在跨子树的耦合关系。&lt;strong&gt;不同子树之间相互独立，决策可以完全分解&lt;/strong&gt; 。由此可通过一次自底向上的深度优先遍历完成全部状态计算，整体时间复杂度为 $O(n)$ 。从结构角度分析，该类问题之所以能够高效求解，根本原因在于树的无环性与层级结构保证了相邻约束的局部性。约束仅在父子节点之间传递，不会形成全局依赖闭环，从而使得整体最优解可以通过对子树最优解的组合得到。&lt;/p&gt;
&lt;h2&gt;没有上司的舞会&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber-iii/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;监控二叉树问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-cameras/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;相邻不同最长路&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;赫奇帕奇的金杯&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF855C&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;换根转移问题&lt;/h2&gt;
&lt;p&gt;在树型动态规划中，&lt;strong&gt;根节点是否固定、以及根的选择是否影响答案&lt;/strong&gt; ，本身就是题目性质的一部分。具体来看，树上 DP 的问题大致可以分为三类：一类是题目 &lt;strong&gt;明确规定了根节点&lt;/strong&gt; ，状态与转移均围绕该根展开；一类是题目 &lt;strong&gt;没有规定根节点，但无论选择哪个节点作为根，最终答案都相同&lt;/strong&gt; ；还有一类则是题目 &lt;strong&gt;既未规定根节点，且不同根对应的答案彼此不同&lt;/strong&gt; 。换根 DP 正是为第三类问题服务的。&lt;/p&gt;
&lt;p&gt;在这类问题中，我们往往需要求出 &lt;strong&gt;以每个节点作为根时的答案&lt;/strong&gt; 。如果对每个节点都重新做一遍树形 DP，时间复杂度通常会膨胀到 $O(n^2)$ 。而换根 DP 的目标，是在 &lt;strong&gt;整体只进行线性规模计算&lt;/strong&gt; 的前提下，高效地完成所有根的切换。从结构上看，换根并不会改变树的拓扑结构，只会改变 &lt;strong&gt;边的方向认知&lt;/strong&gt; ，也就是说原本的父子关系会发生翻转。对某个节点而言，当它成为新的根时，原来属于其父方向的部分，需要被视为一棵新的子树，并与原有的子树一起参与状态计算。因此，换根的本质并不是重新计算整棵树，而是一次 &lt;strong&gt;局部贡献的重新分配&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;典型做法通常分为两个阶段。第一阶段，任选一个节点作为初始根，自底向上进行一次 DFS，计算每个节点在 “只考虑其子树” 的条件下的 DP 值。第二阶段，再进行一次 DFS，将来自父节点方向的补充信息向下传递，使每个节点都能够在常数时间内构造出 “以自己为根” 时所需的完整状态。通过这种方式，每条边只会被正向、反向各处理一次，整体复杂度保持在 $O(n)$ 。&lt;/p&gt;
&lt;p&gt;从更抽象的角度看，换根 DP 解决的是 &lt;strong&gt;状态定义依赖根节点的问题&lt;/strong&gt; 。它要求我们将某个节点的整体状态，拆分为来自各个相邻方向的独立贡献，并保证这些贡献可以被删除、替换和重新组合。一旦这种可分解性成立，根节点的切换就不再需要重做计算，而只是一种视角上的平移。&lt;/p&gt;
&lt;h2&gt;最大深度和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P347&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;染色的最大收益&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF1187E&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;翻转最少的首都&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF219D&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;流量和最大的根&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://poj.org/problem?id=3585&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;每个节点一定距离以内的权值和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3047&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;可改重心的节点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF708C&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;用时最少的节点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P6419&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;树型依赖背包问题&lt;/h1&gt;
&lt;p&gt;树型依赖背包是传统背包模型在拓扑结构上的进阶扩展。与普通背包中物品 “彼此独立、自由选取” 的逻辑不同，这类问题引入了明确的 &lt;strong&gt;依赖约束&lt;/strong&gt;：若要选择某个特定物品，必须先选择其前置物品。这种制约关系排除了任意子集的可能性，要求选择方案必须在结构上构成一个 &lt;strong&gt;包含根节点的连通子图&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;从图论视角看，这种依赖关系通常抽象为一棵树或森林，其中父节点代表前置主件，子节点代表依赖附件。这使得问题的本质转化为：&lt;strong&gt;在树形结构的连通性约束下，寻优满足容量限制的最大价值子集&lt;/strong&gt; 。这种结构特性决定了我们无法通过简单的线性扫描求解，而必须利用树的递归性质，通过树形动态规划将决策逻辑逐层向根节点汇聚。&lt;/p&gt;
&lt;p&gt;在建模时，若依赖关系本身是一棵树，可以直接以根节点为起点进行树形动态规划；若依赖关系是多棵互不相连的树（即森林），则可以引入一个 &lt;strong&gt;虚拟根节点&lt;/strong&gt; ，将所有原本没有父节点的根节点连到该虚根上，并设虚根的体积和价值均为 $0$ 。这样，森林结构就被统一转化为一棵树，从而可以在同一套树形背包框架下处理。&lt;/p&gt;
&lt;p&gt;设节点 $u$ 的子节点依次为 ${v_1, v_2, \dots, v_m}$ 。我们定义三维状态 $dp[u][i][j]$ 表示以节点 $u$ 为根的子树，已经处理完前 $i$ 个子节点（即 $v_1 \sim v_i$ ）且当前容量不超过 $j$ 所能获得的最大价值。&lt;/p&gt;
&lt;p&gt;由于子节点的选择依赖于父节点，因此只有在选择了节点 $u$ 本身之后，才可以考虑其子节点。设节点 $u$ 的体积为 $v_u$ ，价值为 $w_u$ ，则当尚未处理任何子节点（即 $i=0$ ）时，有初始化：&lt;/p&gt;
&lt;p&gt;$$
dp[u][0][j] =
\begin{cases}
w_u &amp;amp; j \ge v_u \
-\infty &amp;amp; j &amp;lt; v_u
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;随后依次合并子节点，当处理第 $i$ 个子节点 $v_i$ 时，需要在当前容量 $j$ 下为该子树分配一定容量 $k$ ，状态转移为：&lt;/p&gt;
&lt;p&gt;$$
dp[u][i][j] = \max_{0 \le k \le j} \Big( dp[u][i-1][j-k] + dp[v_i][m_i][k] \Big)
$$&lt;/p&gt;
&lt;p&gt;其中 $m_i$ 表示节点 $v_i$ 的子节点数，$dp[v_i][m_i][k]$ 表示子树 $v_i$ 在容量 $k$ 下的最优解。通过这种方式，子节点被逐一合并，相当于在节点 $u$ 上执行一次多阶段的分组背包。当所有子节点处理完毕（即 $i = m$ ）后，$dp[u][m][j]$ 即为以 $u$ 为根的整棵子树在容量 $j$ 下的最优值。如果存在虚根，最终答案即为 $dp[0][m_0][V]$ ，其中 $V$ 为背包总容量，$m_0$ 为虚根的子节点个数。&lt;/p&gt;
&lt;p&gt;在时间复杂度方面，引入虚根 &lt;strong&gt;不会改变数量级&lt;/strong&gt; 。设节点总数为 $n$ ，背包容量为 $V$ ，树形背包的主要开销来自对子节点逐一合并时的容量枚举。对于每条父子边，都会进行一次容量划分的转移，整体复杂度通常为 $O(nV^2)$ 。引入虚根后，节点数仅增加 $1$ ，边数增加若干条（等于原森林的树数），只是多进行一次合并操作，因此复杂度仍为 $O(nV^2)$ ，仅常数略有变化。&lt;/p&gt;
&lt;h3&gt;DFN序结构性优化&lt;/h3&gt;
&lt;p&gt;树有一个&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/tree-algorithms/tree-algorithms/#dfn%E5%BA%8F%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98&quot;&gt;非常重要的结构性质&lt;/a&gt;：在先序遍历中，每个节点的整棵子树一定对应一段连续区间。也就是说，如果我们在 DFS 过程中给每个节点一个编号 &lt;code&gt;dfn[u]&lt;/code&gt; ，并记录该节点子树的大小 &lt;code&gt;siz[u]&lt;/code&gt; ，那么节点 $u$ 的整棵子树在 DFN 序中恰好对应区间：&lt;/p&gt;
&lt;p&gt;$$
\Big[ dfn[u],\ dfn[u] + siz[u] - 1 \Big]
$$&lt;/p&gt;
&lt;p&gt;这意味着每个节点都可以在 $O(1)$ 的时间内确定自己子树在序列中的起止位置，原本分支状的树结构，被降维成一个线性数组。换句话说，&lt;strong&gt;树的层级包含关系被完整地编码进了区间的嵌套关系之中&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这种 “子树区间连续性” 使得许多树上问题 &lt;strong&gt;可以自然地转化为区间问题&lt;/strong&gt; 。例如，当我们需要快速查询某个节点子树的信息、对子树进行批量修改，甚至删除或统计整棵子树时，都可以先通过 DFS 编号将问题映射到区间上，再借助线段树、树状数组等线性数据结构进行维护。原本依赖递归遍历的操作，被替换为对一段连续区间的直接处理，结构更加规整，复杂度也更容易控制。&lt;/p&gt;
&lt;p&gt;将树结构线性化之后，带依赖背包同样可以进行结构层面的重构。传统树形背包是在每个节点处逐个合并子节点，本质是多次分组背包的嵌套进行。而在 DFN 序下，整棵子树已经对应为一个连续区间，因此我们可以尝试直接在 DFN 序上进行线性动态规划。&lt;strong&gt;原本的层级合并，被转化为顺序决策问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;设整棵树按 DFS 序重新编号后，节点编号为 $1 \sim n$ 。由于 DFS 的访问顺序保证了一个节点的所有子节点一定出现在它之后，并且整棵子树一定是一个连续的区间，当我们处理到编号 $i$ 时，若选择该节点，就必须覆盖 &lt;strong&gt;整棵子树对应的区间&lt;/strong&gt; 。同时，为保证转移所需的状态已经计算完成，我们需要对 $i$ 进行 &lt;strong&gt;倒序枚举&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;于是定义状态 $dp[i][j]$ 表示从 DFN 序位置 $i$ 开始考虑，在当前容量不超过 $j$ 的情况下所能获得的最大价值。接下来的关键在于状态转移的设计，当处理到节点 $u$（其 DFN 编号为 $i$ ）时，有两种决策：不选该节点，则直接跳过整棵子树；选该节点，则消耗 $v_u$ 的容量，获得 $w_u$ 的价值，并继续考虑其子树内部的选择。&lt;/p&gt;
&lt;p&gt;由于子树在 DFS 序中对应区间：&lt;/p&gt;
&lt;p&gt;$$
\Big[ dfn[u],\ dfn[u] + siz[u] - 1 \Big]
$$&lt;/p&gt;
&lt;p&gt;若不选节点 $u$ ，则必须直接跳转到：&lt;/p&gt;
&lt;p&gt;$$
i&apos; = dfn[u] + siz[u]
$$&lt;/p&gt;
&lt;p&gt;因为其所有子节点在依赖约束下都无法被选择。于是转移可写为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max \Big( dp[i + siz[u]][j], , \max_{j \geq v_u} \left( dp[i+1][j - v_u] + w_u \right) \Big)
$$&lt;/p&gt;
&lt;p&gt;第一项表示不选当前节点，直接跳过整棵子树；第二项表示选当前节点，然后继续在 DFN 序列中处理其子节点。可以看到，原本子树合并的过程，被替换为区间跳跃的过程。树的依赖关系不再通过嵌套循环进行维护，而是通过 DFN 序的区间结构保证合法性，&lt;strong&gt;一旦父节点未被选择，其整棵子树会被整体跳过，从而自动满足依赖约束&lt;/strong&gt; 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;树型依赖背包 DFN 序优化的详细讲解可以看下面这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=236257271&amp;amp;bvid=BV1ae411f7AC&amp;amp;cid=34362950586&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;这种写法的本质是将树形动态规划改写为带区间跳跃的线性动态规划，由于每个节点仅被处理一次，不再需要在每个父节点处反复枚举子节点进行合并，状态规模仅为 $O(nV)$ 。相比传统的 $O(nV^2)$ 合并式写法，这种结构更便于使用滚动数组进行空间压缩优化。从结构角度看，传统树形背包是自底向上逐层汇总的过程，而 DFN 优化后的写法则是沿 DFN 序的倒序扫描。&lt;/p&gt;
&lt;h2&gt;大学生最优选课&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2014&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法随笔】动态规划思想</title><link>https://xingguang641.com/posts/acm/acm-note/dynamic-programming/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/dynamic-programming/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇博客写作灵感来源于 N 神的动态规划概述&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114531325973175&amp;amp;bvid=BV1m9EUzLEB4&amp;amp;cid=30031939563&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h1&gt;动态规划状态设计&lt;/h1&gt;
&lt;p&gt;动态规划建模的核心难点往往不在于转移公式的推导，而在于对 &lt;strong&gt;状态的精准刻画&lt;/strong&gt; 。状态设计的本质是信息的一场取舍游戏：它决定了哪些关键变量必须被显式记录以维持逻辑完备，哪些冗余信息可以被忽略以降低搜索开销。一个精妙的状态定义能让转移路径清晰可见，而一个臃肿的定义则会使算法的复杂度飙升。这种状态设计的本质是在 &lt;strong&gt;可行解空间的完备性&lt;/strong&gt; 与 &lt;strong&gt;计算资源的约束性&lt;/strong&gt; 之间寻求动态平衡，从而确保决策过程始终沿着一条逻辑封闭且单调收敛的路径稳步推进。&lt;/p&gt;
&lt;p&gt;从实际建模的视角来看，状态设计实际上是对于 &lt;strong&gt;状态空间规模与信息完整性的系统化建模&lt;/strong&gt; 。为了确保决策符合 &lt;strong&gt;无后效性&lt;/strong&gt; 原则，我们有时需要通过增加维度来记录必要的历史信息；而在面临复杂度挑战时，又必须深入剖析问题的内在属性，通过归纳实现降维。从高层逻辑审视，状态本质上是高维可行解空间向低维层面的 &lt;strong&gt;投影与重构&lt;/strong&gt;：每个维度对应一个核心约束或度量标准，设计的关键在于甄别哪些变量必须作为状态参数显式记录，而哪些可以通过最优性或单调特征进行合并。&lt;/p&gt;
&lt;h2&gt;维度转置与压缩&lt;/h2&gt;
&lt;p&gt;在许多动态规划问题中，建模的本质在于对多维属性（如资源消耗与目标收益）进行合理编排。最直观的建模方式是 &lt;strong&gt;直接对应题目限制&lt;/strong&gt; ，即固定资源为状态维度，以收益为优化目标。例如使用 $dp[i][j] = k$ 来表示在前 $i$ 次决策中，在资源消耗为 $j$ 的前提下所能达到的最大收益 $k$ 。这种表达方式完美契合了我们求解最优值的习惯。&lt;/p&gt;
&lt;p&gt;当资源的取值范围过大时，上述的建模方式会因为 &lt;strong&gt;状态空间的膨胀&lt;/strong&gt; 导致难以维护。此时通过维度转置，即交换收益与资源的位置，我们可以得到更优的状态结构。例如使用 $dp[i][k] = j$ 来表示在前 $i$ 次决策中，达到收益 $k$ 时所需的最小资源开销 $j$ 。这种转变将数值过大的资源作为状态值，从而保持逻辑的有效性与计算的可行性。&lt;/p&gt;
&lt;p&gt;从信息层面重新审视，这类最优化问题的本质就是该三维可行性分布：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j][k] =
\begin{cases}
1 &amp;amp; \text{if reachable} \
0 &amp;amp; \text{if unreachable}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;这三维结构涵盖了问题 &lt;strong&gt;全部的自由度&lt;/strong&gt; ，完整地定义了状态空间。而前述两种常见的二维模型，本质上都是该三维结构在不同约束下的投影，即通过极值运算将三维的可行性分布 &lt;strong&gt;坍缩为二维的最优值映射&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max { k \mid dp[i][j][k] = 1 }, \quad dp[i][k] = \min { j \mid dp[i][j][k] = 1 }
$$&lt;/p&gt;
&lt;p&gt;在进阶建模技巧中，一种通用且实用的解题思路是：当面对复杂问题无法直接定义出紧凑的状态时，可以先将问题的所有自由度 &lt;strong&gt;显式展开为高维状态&lt;/strong&gt; ，随后再通过分析问题的单调性或极值结构，将冗余的维度转化为状态值。这种 &lt;strong&gt;由繁入简&lt;/strong&gt; 的构建过程，本质上是对状态空间的重塑，它将原本复杂的状态定义问题转化为对维度冗余的识别与压缩，从而帮助我们从全维度的视角出发，推导出结构清晰且复杂度可控的递推模型。&lt;/p&gt;
&lt;h2&gt;最小贿赂金币数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/736e12861f9746ab8ae064d4aae2d5a9&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你正在面对 $n$ 只怪兽，必须按 &lt;strong&gt;从左到右&lt;/strong&gt; 的顺序依次通过。每只怪兽有两个属性：能力值 $a_i$ 和贿赂它所需的钱数 $b_i$ 。开始时你的能力为 $0$ 。&lt;/p&gt;
&lt;p&gt;对于每一只怪兽，你的通过规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必须贿赂&lt;/strong&gt;：如果你当前的能力 &lt;strong&gt;小于&lt;/strong&gt; $i$ 号怪兽的能力，则你必须付出 $b_i$ 的钱贿赂这只怪兽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以选择贿赂&lt;/strong&gt;：如果你当前的能力 &lt;strong&gt;大于等于&lt;/strong&gt; $i$ 号怪兽的能力，你可以选择直接通过（不花钱，能力不增加），也可以选择依然付出 $b_i$ 的钱贿赂这只怪兽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贿赂的效果&lt;/strong&gt;：如果你贿赂了怪兽，怪兽会加入你的队伍，其能力 $a_i$ 会直接累加到你的当前能力上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你的目标是按顺序通过所有 $n$ 只怪兽。请计算通关所需的 &lt;strong&gt;最小钱数&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 1000$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i \leq 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq b_i \leq 10$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示怪兽的数量。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行包含两个整数 $a_i$ 和 $b_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad b_1$&lt;/p&gt;
&lt;p&gt;$a_2 \quad b_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$a_n \quad b_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示通关所需的最小钱数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
8 10
6 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;动态规划的核心挑战在于如何设计一个既能涵盖所有必要决策信息，又能在时空复杂度限制内运行的状态表示。针对本题，如果按照常规思维，将 &lt;strong&gt;当前积累的能力值&lt;/strong&gt; 作为 DP 的一个维度（即定义 $dp[i][j]$ 为面对前 $i$ 只怪兽、当前能力为 $j$ 时的最小花费），会直接面临复杂度过高的问题。由于单只怪兽的能力值 $a_i$ 最高可达 $10^4$ ，总能力上限在 $10^7$ 数量级，这种设计会产生巨大的状态空间，从而造成严重的内存溢出。&lt;/p&gt;
&lt;p&gt;为了优化模型，我们可以利用 &lt;strong&gt;维度转置&lt;/strong&gt; 的思想，将状态维度与数值角色互换，转而定义 &lt;strong&gt;$dp[j]$ 为花费恰好 j 元钱时所能积累的最大能力值&lt;/strong&gt; 。由于题目中总花费的上限可控，该维度的空间开销非常理想。在此框架下，判定能否通过某只怪兽的标准从原本求解最小钱数转变为验证在给定花费下所能达到的最大能力是否足以覆盖怪兽防御力，这种转换直接摒弃了对原始庞大能力空间的直接枚举，转而将求解重心转移至取值更小且可控的花费维度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1005;
const int MAXM = 10005;
const ll INF = 0xcfcfcfcfcfcfcfcfLL;
ll dp[MAXN][MAXM];

int main() {
    int n; cin &amp;gt;&amp;gt; n;

    for (int i = 0; i &amp;lt;= n; i++) {
        for (int j = 0; j &amp;lt;= 10 * n; j++) {
            dp[i][j] = INF;
        }
    }
    dp[0][0] = 0;

    for (int i = 1; i &amp;lt;= n; i++) {
        ll a; int b;
        cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
        for (int j = 0; j &amp;lt;= 10 * n; j++) {
            if (j &amp;gt;= b &amp;amp;&amp;amp; dp[i - 1][j - b] != INF) {
                dp[i][j] = max(dp[i][j], dp[i - 1][j - b] + a);
            }

            if (dp[i - 1][j] &amp;gt;= a) {
                dp[i][j] = max(dp[i][j], dp[i - 1][j]);
            }
        }
    }

    for (int j = 0; j &amp;lt;= 10 * n; j++) {
        if (dp[n][j] &amp;gt;= 0) {
            cout &amp;lt;&amp;lt; j &amp;lt;&amp;lt; endl;
            break;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自由度约简技巧&lt;/h2&gt;
&lt;p&gt;在设计动态规划模型时，初学者常倾向于将所有变量直接映射为状态维度，这极易导致 &lt;strong&gt;维度爆炸&lt;/strong&gt; ，使状态空间超出计算承载极限。事实上，许多看似独立的变量间往往存在隐含的 &lt;strong&gt;约束关系&lt;/strong&gt; ，通过挖掘这些信息来剔除冗余维度，便是自由度约简的核心。这种方法利用变量间 &lt;strong&gt;依赖关系&lt;/strong&gt; ，仅需记录关键变量即可推导出其余变量。这不仅能有效简化状态表示，更是对问题本质结构的深度洞察，要求我们跳出直观记录的思维定式，寻找变量内部的 &lt;strong&gt;关联性&lt;/strong&gt; ，从而构建出更高效的模型。&lt;/p&gt;
&lt;p&gt;该技巧在处理多对象同起点且同步移动的问题尤为经典。例如，若有两个棋子同时从起点 $(0,0)$ 出发，每步均只能向右或向下移动，那么在任何时刻，这两个棋子的坐标 $(x_1, y_1)$ 和 $(x_2, y_2)$ 必然满足 &lt;strong&gt;步数守恒&lt;/strong&gt; ，即 $x_1 + y_1 = x_2 + y_2 = step$ 。这意味着我们无需记录完整的四个坐标，仅需维护当前的步数 $step$ 以及两者的横坐标 $x_1$ 和 $x_2$ ，剩下的纵坐标 $y_1$ 和 $y_2$ 可通过 $step - x$ 直接推导得出。通过这种 &lt;strong&gt;降维处理&lt;/strong&gt; ，原本冗余的状态空间被大幅压缩，使得原本计算代价极高的状态转移过程，变成了一个易于实现且运行高效的 &lt;strong&gt;低维递推方程&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;往返摘樱桃难题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/cherry-pickup/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;一个 $n \times n$ 的网格 &lt;code&gt;grid&lt;/code&gt; 代表棋盘，每个单元格内容可以是以下三种之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;：表示该单元格是空的，可以穿过。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;：表示该单元格包含一个樱桃，可以在经过时摘取。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-1&lt;/code&gt;：表示该单元格包含一个障碍物，无法穿过。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从起点 $(0, 0)$ 出发，只能向 &lt;strong&gt;右&lt;/strong&gt; 或向 &lt;strong&gt;下&lt;/strong&gt; 移动，直到到达终点 $(n-1, n-1)$ 。&lt;/li&gt;
&lt;li&gt;在经过包含樱桃的单元格时，摘取樱桃，该单元格随后变为 &lt;code&gt;0&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;到达 $(n-1, n-1)$ 后，从该点出发，只能向 &lt;strong&gt;左&lt;/strong&gt; 或向 &lt;strong&gt;上&lt;/strong&gt; 移动，直到回到起点 $(0, 0)$ 。&lt;/li&gt;
&lt;li&gt;同样，在回程经过包含樱桃的单元格时，摘取樱桃（如果第一次经过时已经摘取，则此处为 &lt;code&gt;0&lt;/code&gt; ）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请计算你最多能摘取的樱桃数。如果不存在一条合法的路径，则返回 $0$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == grid.length$&lt;/li&gt;
&lt;li&gt;$n == grid[i].length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 50$&lt;/li&gt;
&lt;li&gt;$grid[i][j]$ 为 $-1, 0, 1$&lt;/li&gt;
&lt;li&gt;$grid[0][0] \neq -1$&lt;/li&gt;
&lt;li&gt;$grid[n-1][n-1] \neq -1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示网格的大小。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行 $n$ 个整数，表示网格数组 $grid_{i,j}$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$grid_{1,1} \quad grid_{1,2} \quad \ldots \quad grid_{1,n}$&lt;/p&gt;
&lt;p&gt;$\dots$&lt;/p&gt;
&lt;p&gt;$grid_{n,1} \quad grid_{n,2} \quad \ldots \quad grid_{n,n}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多能摘取的樱桃总数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
0 1 -1
1 0 -1
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;扰乱字符串问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/scramble-string/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;使用下面描述的算法可以扰乱字符串 $s$ 得到字符串 $t$ ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果字符串的长度为 $1$ ，算法停止。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果字符串的长度 $&amp;gt; 1$ ，执行下述步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在一个随机下标处将字符串分割成两个非空的子字符串。即，如果已知字符串 $s$ ，则可以将其分成 $x$ 和 $y$ ，且满足 $s = x + y$ 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机&lt;/strong&gt; 决定是否交换这两个子字符串。若交换，则 $s$ 变成 $y + x$ ；若不交换，则 $s$ 变成 $x + y$ 。&lt;/li&gt;
&lt;li&gt;应用该算法继续递归地对两个子字符串进行扰乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;给你两个 &lt;strong&gt;长度相等&lt;/strong&gt; 的字符串 $s1$ 和 $s2$ ，判断 $s2$ 是否是 $s1$ 的扰乱字符串。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$s1.length == s2.length$&lt;/li&gt;
&lt;li&gt;$1 \leq s1.length \leq 30$&lt;/li&gt;
&lt;li&gt;$s1$ 和 $s2$ 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 $s1$ ，表示原始字符串。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串 $s2$ ，表示待检查的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s1$&lt;/p&gt;
&lt;p&gt;$s2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果 $s2$ 是 $s1$ 的扰乱字符串，输出 &lt;code&gt;true&lt;/code&gt; ；否则输出 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;great
rgeat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abcde
caebd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;状态主维度理论&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;动态规划建模技巧&lt;/h1&gt;
&lt;p&gt;动态规划的核心价值在于 &lt;strong&gt;对复杂问题进行形式化抽象与结构化表达的能力&lt;/strong&gt; 。许多问题在表层呈现出明显的递推特征，但若仅凭直观感性去构造状态，往往会导致 &lt;strong&gt;状态空间规模爆炸&lt;/strong&gt; 、依赖关系错综复杂或转移机制难以精确刻画。因此，动态规划进阶的关键是在深挖问题内在逻辑的基础上，构建一套能够准确反映子问题演化规律的 &lt;strong&gt;状态表示体系&lt;/strong&gt; 。所谓建模技巧，本质上是对问题表达框架的 &lt;strong&gt;重构过程&lt;/strong&gt; ，在确保原问题语义不变的前提下，通过视角切换或变量重构，使问题结构更契合递推分析的需求。&lt;/p&gt;
&lt;p&gt;例如，将单点往返条件重构为 &lt;strong&gt;双点同步推进&lt;/strong&gt; ，可以将原本具有时间先后依赖的折返过程转化为并行过程，从而消除跨阶段的逻辑耦合；又如在网格类问题中，通过将坐标系旋转四十五度，可以将原本受限于对角线约束的 &lt;strong&gt;曼哈顿距离问题&lt;/strong&gt; 转化为新坐标系下的简单表达。通过对这些转化方式进行归纳与提炼，我们可以在面对形式多变的问题时，更敏锐地识别其深层的 &lt;strong&gt;递推逻辑&lt;/strong&gt; ，从而构造出逻辑严密、表达简洁且复杂度可控的动态规划模型。&lt;/p&gt;
&lt;h2&gt;最少的跳跃能力&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8775&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;小青蛙库睿奇要过河去参加派对。河的宽度为 $n$ ，河上分布着 $n-1$ 块石头，第 $i$ 块石头距离河左岸的距离为 $i$ ，其高度为 $h_i$ 。由于高度限制，每块石头最多只能被踩 $h_i$ 次。&lt;/p&gt;
&lt;p&gt;库睿奇准备从左岸跳到右岸，再从右岸跳回左岸，如此往返共 $2x$ 次（即 $x$ 次从左往右，$x$ 次从右往左）。&lt;/p&gt;
&lt;p&gt;在跳跃过程中，库睿奇的能力值为 $y$ ，这意味着它每次跳跃的距离 &lt;strong&gt;不能超过&lt;/strong&gt; $y$ 。请计算库睿奇能够完成 $2x$ 次跳跃所需的 &lt;strong&gt;最小能力值 y&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq x \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq h_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $x$ ，分别表示河的宽度和往返的总次数。&lt;/li&gt;
&lt;li&gt;第二行包含 $n-1$ 个整数 $h_1, h_2, \ldots, h_{n-1}$ ，表示每块石头的高度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad x$&lt;/p&gt;
&lt;p&gt;$h_1 \quad h_2 \quad \ldots \quad h_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示要求的最小能力值 $y$ 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 1
1 0 1 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;过河所需石子数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1052&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在河上有一座独木桥，长度为 $L$ 。桥上分布着 $M$ 颗石子，每颗石子所在的坐标都是 $1$ 到 $L-1$ 之间的整数。青蛙库里奇准备从桥的起点（坐标 $0$ ）跳到桥的终点（坐标 $L$ 或更远的地方）。库里奇每次跳跃的距离是 $[S, T]$ 之间的任意整数。&lt;/p&gt;
&lt;p&gt;在跳跃过程中，如果库里奇落下的位置刚好有一颗石子，它就会踩到这颗石子。库里奇希望在顺利过河的前提下，&lt;strong&gt;踩到的石子数量最少&lt;/strong&gt; 。请计算出库里奇过河所需踩到的最少石子数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq L \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq S \leq T \leq 10$&lt;/li&gt;
&lt;li&gt;$1 \leq M \leq 100$&lt;/li&gt;
&lt;li&gt;石子坐标在 $(0, L)$ 范围内&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $L$ ，表示桥的长度。&lt;/li&gt;
&lt;li&gt;第二行包含三个整数 $S$ 、$T$ 和 $M$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $M$ 个整数，表示桥上每颗石子的坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$L$&lt;/p&gt;
&lt;p&gt;$S \quad T \quad M$&lt;/p&gt;
&lt;p&gt;$x_1 \quad x_2 \quad \ldots \quad x_M$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最少踩到的石子数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
2 3 5
2 3 5 6 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;动态规划递归转化&lt;/h1&gt;
&lt;p&gt;从算法建模的角度看，&lt;strong&gt;绝大多数动态规划问题在本质上都可以通过递归形式进行刻画&lt;/strong&gt; 。递归的核心价值在于清晰地定义了问题的 &lt;strong&gt;最优子结构&lt;/strong&gt;：即一个规模为 $n$ 的复杂问题，其最优解能够被分解为若干规模更小的子问题解的组合。因此，在构建任何动态规划模型时，优先以 &lt;strong&gt;递归视角&lt;/strong&gt; 明确状态的定义及其相互依赖关系，是揭示问题底层逻辑最直观的途径。&lt;/p&gt;
&lt;p&gt;虽然朴素递归能完整描述求解逻辑，但其最大的弊端在于产生大量的 &lt;strong&gt;重复子问题&lt;/strong&gt; 。在递归树的展开过程中，不同分支往往会多次触达完全相同的状态。当状态空间有限且计算结果具有确定性时，我们可以引入缓存机制，即 &lt;strong&gt;记忆化搜索（Memoization）&lt;/strong&gt;。通过记录已求解状态的结果，计算效率可以实现从指数级到多项式级的质变。在这个意义上，记忆化搜索正是 &lt;strong&gt;自顶向下&lt;/strong&gt; 实现动态规划的核心手段。&lt;/p&gt;
&lt;h2&gt;一年的火车旅行&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-cost-for-tickets/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在一个火车旅行很受欢迎的国度，你提前一年计划了一些火车旅行。在接下来的一年里，日历第 &lt;code&gt;days[i]&lt;/code&gt; 天是你将会进行旅行的日子。这些天数按 &lt;strong&gt;升序&lt;/strong&gt; 给出。&lt;/p&gt;
&lt;p&gt;火车票有 &lt;strong&gt;三种不同的销售方式&lt;/strong&gt; ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;1 天通行证&lt;/strong&gt; ：售价为 &lt;code&gt;costs[0]&lt;/code&gt; 美元，允许你在 1 天内不限次数地乘坐火车。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7 天通行证&lt;/strong&gt; ：售价为 &lt;code&gt;costs[1]&lt;/code&gt; 美元，允许你在 7 天内（包含开始的那天）不限次数地乘坐火车。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;30 天通行证&lt;/strong&gt; ：售价为 &lt;code&gt;costs[2]&lt;/code&gt; 美元，允许你在 30 天内（包含开始的那天）不限次数地乘坐火车。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;返回你想要完成所有计划日期内旅行所需的 &lt;strong&gt;最低消费&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq days.length \leq 365$&lt;/li&gt;
&lt;li&gt;$1 \leq days[i] \leq 365$&lt;/li&gt;
&lt;li&gt;$days$ 按 &lt;strong&gt;升序&lt;/strong&gt; 排列&lt;/li&gt;
&lt;li&gt;$costs.length == 3$&lt;/li&gt;
&lt;li&gt;$1 \leq costs[i] \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含若干个整数，表示计划旅行的日子。&lt;/li&gt;
&lt;li&gt;第二行包含三个整数，表示三种通行证的价格。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$days_1 \quad days_2 \quad \ldots \quad days_n$&lt;/p&gt;
&lt;p&gt;$costs_0 \quad costs_1 \quad costs_2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含一个整数，表示最低花费。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 4 6 7 8 20
2 7 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;11
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2 3 4 5 6 7 8 9 10 30 31
2 7 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;17
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;简单的解码方法&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/decode-ways/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;一条包含字母 &lt;code&gt;A-Z&lt;/code&gt; 的消息通过以下映射进行了 &lt;strong&gt;编码&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;A&apos; -&amp;gt; &quot;1&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;B&apos; -&amp;gt; &quot;2&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;Z&apos; -&amp;gt; &quot;26&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要 &lt;strong&gt;解码&lt;/strong&gt; 已编码的消息，所有数字必须分组，然后按上述映射逆向映射回字母。例如 &lt;code&gt;&quot;11106&quot;&lt;/code&gt; 可以映射为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;AAJF&quot;&lt;/code&gt; ，将消息分组为 &lt;code&gt;(1, 1, 10, 6)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;KJF&quot;&lt;/code&gt; ，将消息分组为 &lt;code&gt;(11, 10, 6)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，消息不能分组为 &lt;code&gt;(1, 11, 06)&lt;/code&gt; ，因为 &lt;code&gt;&quot;06&quot;&lt;/code&gt; 不能映射为 &lt;code&gt;&quot;F&quot;&lt;/code&gt; ，由于 &lt;code&gt;&quot;6&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;06&quot;&lt;/code&gt; 在映射中是不同的。&lt;/p&gt;
&lt;p&gt;给你一个只含数字的 &lt;strong&gt;非空&lt;/strong&gt; 字符串 $s$ ，请计算并返回 &lt;strong&gt;解码&lt;/strong&gt; 方法的 &lt;strong&gt;总数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;题目数据保证答案肯定是一个 &lt;strong&gt;32 位&lt;/strong&gt; 的整数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 100$&lt;/li&gt;
&lt;li&gt;$s$ 只包含数字，并且可能包含前导零&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含一个整数，表示解码方法的总数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;226
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;06
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;困难的解码方式&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/decode-ways-ii/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;一条包含字母 &lt;code&gt;A-Z&lt;/code&gt; 的消息通过以下映射进行了 &lt;strong&gt;编码&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;A&apos; -&amp;gt; &quot;1&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;B&apos; -&amp;gt; &quot;2&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;Z&apos; -&amp;gt; &quot;26&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了数字之外，已编码的消息还可以包含 &lt;code&gt;&apos;*&apos;&lt;/code&gt; 字符，该字符可以表示从 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 到 &lt;code&gt;&apos;9&apos;&lt;/code&gt; 的任意数字。例如，&lt;code&gt;&quot;1*&quot;&lt;/code&gt; 可以表示从 &lt;code&gt;&quot;11&quot;&lt;/code&gt; 到 &lt;code&gt;&quot;19&quot;&lt;/code&gt; 的任何编码消息。&lt;/p&gt;
&lt;p&gt;要 &lt;strong&gt;解码&lt;/strong&gt; 一条消息，所有数字必须分组，然后按上述映射逆向映射回字母。&lt;/p&gt;
&lt;p&gt;给你一个字符串 $s$ ，由数字和 &lt;code&gt;&apos;*&apos;&lt;/code&gt; 字符组成，返回 &lt;strong&gt;解码&lt;/strong&gt; 该消息的 &lt;strong&gt;总数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;由于答案可能会非常大，所以必须对 $10^9 + 7$ 取模。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$s[i]$ 是数字或 &lt;code&gt;&apos;*&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含一个整数，表示解码方法的总数对 $10^9 + 7$ 取模后的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;18
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;骑士的存活概率&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/knight-probability-in-chessboard/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在一个 $n \times n$ 的国际象棋棋盘上，一个骑士从单元格 $(row, column)$ 开始，并尝试进行 $k$ 次移动。行和列从 &lt;strong&gt;0&lt;/strong&gt; 开始计数，所以左上角的单元格为 $(0, 0)$，右下角的单元格为 $(n-1, n-1)$。&lt;/p&gt;
&lt;p&gt;象棋骑士有 $8$ 种可能的移动方式。每次移动在 $L$ 形方向上前进：选择一个方向（上下左右）走 2 格，然后垂直于该方向走 $1$ 格。每当骑士需要移动时，它会从 $8$ 种可能的移动中 &lt;strong&gt;等概率&lt;/strong&gt; 地选择一种（即使棋子移动后会离开棋盘），然后移动到那里。&lt;/p&gt;
&lt;p&gt;骑士继续移动，直到它完成了 $k$ 次移动或跳出了棋盘。返回骑士在完成 $k$ 次移动后仍留在棋盘上的 &lt;strong&gt;概率&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 25$&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq row, column \leq n - 1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示棋盘的边长。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $k$ ，表示骑士的移动次数。&lt;/li&gt;
&lt;li&gt;第三行包含两个整数 $row$ 和 $column$ ，表示骑士的起始位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$k$&lt;/p&gt;
&lt;p&gt;$row \quad column$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个浮点数，表示骑士最终仍停留在棋盘上的概率。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2
0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0.06250
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
0
0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1.00000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;动态规划经典重构&lt;/h1&gt;
&lt;p&gt;动态规划的精髓在于通过 &lt;strong&gt;状态划分&lt;/strong&gt; 与 &lt;strong&gt;递推关系&lt;/strong&gt; 刻画子问题的依赖结构。在入门实践中，将问题归类为背包、区间或树形等 &lt;strong&gt;经典模型&lt;/strong&gt; 是构建建模思维的基础。然而在竞赛算法的学习中，我们经常会遇到一类题目：它们虽然在表象上与经典模型高度相似，但数据规模却大幅超出了经典范式的承载极限。面对这种显著的数据规模差异，经典算法直接失效，这就要求我们必须跨越模板的思维定式，深入剖析题目本质。这类问题的核心难点在于如何高效地挖掘并利用题目与经典模型之间的细微差异，从而建立一套针对性的破解方案。&lt;/p&gt;
&lt;p&gt;处理此类难题的关键在于寻找经典模型之外的 &lt;strong&gt;特殊限制&lt;/strong&gt; ，并将这些特殊限制作为突破口。当数据规模超出常规算法的承载范围时，题目中通常包含额外的条件约束，例如 &lt;strong&gt;极小的值域范围&lt;/strong&gt; ，或者将输入数组 &lt;strong&gt;限定为排列&lt;/strong&gt; 。深入挖掘这些特性可以 &lt;strong&gt;改变状态转移结构&lt;/strong&gt; ，进而对算法做出 &lt;strong&gt;针对性的优化或重构&lt;/strong&gt; 。通过这些手段，原本难以承受的计算代价能够被降低到合理范围，确保算法在处理大规模数据时依然能够 &lt;strong&gt;高效运行&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;最长递增子排列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1439&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给出两个长度为 $n$ 的排列 $P_1$ 和 $P_2$ ，计算它们的最长公共子序列的长度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排列&lt;/strong&gt; 是指 $1$ 到 $n$ 这 $n$ 个整数每个数恰好出现一次的序列。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$P_1, P_2$ 均为 $1$ 到 $n$ 的排列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示排列的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示排列 $P_1$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示排列 $P_2$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$b_1 \quad b_2 \quad \ldots \quad b_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最长公共子序列的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
3 2 1 4 5
1 2 3 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;假最长公共子序列问题&lt;/p&gt;
&lt;h2&gt;使集合总和相近&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class087/Code02_PickNumbersClosedSum.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定正整数 $n$ 和 $k$ ，你需要从 $1$ 到 $n$ 的整数中选择 $k$ 个数字组成集合 $A$ ，剩下的 $n-k$ 个数字组成集合 $B$ 。目标是使得集合 $A$ 中所有元素的累加和与集合 $B$ 中所有元素的累加和之差的绝对值不超过 $1$ 。&lt;/p&gt;
&lt;p&gt;如果存在满足条件的方案，返回集合 $A$ 中所选的所有数字；如果无法做到，返回一个长度为 $0$ 的数组。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 10^6$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行整数，表示集合 $A$ 中所选的所有数字。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;假01背包问题&lt;/p&gt;
&lt;h2&gt;最优的部署方案&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class128/Code02_BestDeploy1.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $n$ 台机器排成一排，编号为 $1$ 到 $n$。你需要依次部署这些机器，且可以自由决定部署的顺序，最终所有机器都需要被部署。&lt;/p&gt;
&lt;p&gt;给定三个数组 &lt;code&gt;no[]&lt;/code&gt;、&lt;code&gt;one[]&lt;/code&gt; 和 &lt;code&gt;both[]&lt;/code&gt;，对于每一台机器 $i$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no[i]&lt;/code&gt;：当第 $i$ 号机器部署时，如果其相邻的机器均未部署，此时获得的收益。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;one[i]&lt;/code&gt;：当第 $i$ 号机器部署时，如果其相邻的机器中恰有一台已部署，此时获得的收益。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;both[i]&lt;/code&gt;：当第 $i$ 号机器部署时，如果其相邻的机器中已有两台已部署，此时获得的收益。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：第 $1$ 号机器和第 $n$ 号机器在部署时，相邻的已部署机器最多只有一台。请计算并返回部署所有机器能获得的最大收益。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq no[i], one[i], both[i] \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含四行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示 $no$ 数组。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示 $one$ 数组。&lt;/li&gt;
&lt;li&gt;第四行包含 $n$ 个整数，表示 $both$ 数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$\text{no}_1 \quad \text{no}_2 \quad \ldots \quad \text{no}_n$&lt;/p&gt;
&lt;p&gt;$\text{one}_1 \quad \text{one}_2 \quad \ldots \quad \text{one}_n$&lt;/p&gt;
&lt;p&gt;$\text{both}_1 \quad \text{both}_2 \quad \ldots \quad \text{both}_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示部署所有机器能获得的最大收益。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;假区间dp问题&lt;/p&gt;
&lt;h2&gt;增加限制的最长公共子序列问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class128/Code03_AddLimitLcs.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定两个只由小写字母组成的字符串 $s_1$ 和 $s_2$ ，其中 $s_1$ 的长度为 $n$ ，$s_2$ 的长度为 $m$ 。请计算并返回这两个字符串的 &lt;strong&gt;最长公共子序列&lt;/strong&gt; 的长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^6$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 10^3$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 $s_1$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串 $s_2$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s_1$&lt;/p&gt;
&lt;p&gt;$s_2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示两个字符串的最长公共子序列长度。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;假最长公共子序列问题&lt;/p&gt;
&lt;h2&gt;树上最大异或和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4551&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一棵 $n$ 个点的带权树，结点下标从 $1$ 开始到 $n$ 。求树中所有异或路径的最大值。&lt;/p&gt;
&lt;p&gt;异或路径指树上两个结点之间唯一路径上的所有边权的异或值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq u_i, v_i \leq n$&lt;/li&gt;
&lt;li&gt;$0 \leq w &amp;lt; 2^{31}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示结点数。&lt;/li&gt;
&lt;li&gt;接下来 $n - 1$ 行，每行包含三个整数 $u$ 、$v$ 和 $w$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1 \quad w_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2 \quad w_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_{n-1} \quad v_{n-1} \quad w_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 2 3
2 3 4
2 4 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;假树型dp问题&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;经典的DP分类&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/knapsack-dp/&quot;&gt;【ACM 算法题单】背包动态规划相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/interval-dp/&quot;&gt;【ACM 算法题单】区间动态规划相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/tree-dp/&quot;&gt;【ACM 算法题单】树型动态规划相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/state-dp/&quot;&gt;【ACM 算法题单】状压动态规划相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/digit-dp/&quot;&gt;【ACM 算法题单】数位动态规划相关问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;经典的DP问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/maximum-subarray-sum/&quot;&gt;【ACM 算法题单】子数组最大累加和问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-common-subsequence/&quot;&gt;【ACM 算法题单】最长公共子序列问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-increasing-subsequence/&quot;&gt;【ACM 算法题单】最长递增子序列问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/integer-partition/integer-partition/&quot;&gt;【ACM 算法题单】整数拆分问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/regular-bracket/&quot;&gt;【ACM 算法题单】有效括号问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;经典的DP优化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-optimization/monotonic-structure/&quot;&gt;【ACM 算法题单】单调数据结构优化问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-optimization/interval-structure/&quot;&gt;【ACM 算法题单】区间数据结构优化问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】整数拆分问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-problems/integer-partition/integer-partition/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-problems/integer-partition/integer-partition/</guid><description>记录一些 ACM 常见题型</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;经典动态规划思想&lt;/h1&gt;
&lt;p&gt;在组合计数与动态规划问题中，&lt;strong&gt;将整体拆分为若干部分并进行统计&lt;/strong&gt; 是一类非常常见的模型，而整数拆分正是其中最基础、也最具代表性的一个问题。&lt;a href=&quot;https://leetcode.cn/problems/integer-break/&quot;&gt;整数拆分问题&lt;/a&gt;通常是指：将一个正整数拆分为 &lt;strong&gt;恰好 n 个&lt;/strong&gt; 正整数之和，并统计有多少种不同的拆分方式，且不同的拆分顺序视为一种方案。形式化地，我们需要统计满足以下条件的方案数：&lt;/p&gt;
&lt;p&gt;$$
a_1 + a_2 + \dots + a_n = S \quad a_i \geq 1
$$&lt;/p&gt;
&lt;p&gt;针对经典的整数拆分问题，我们通常定义二元状态 $dp[i][j]$ 表示将正整数 $i$ 拆分为恰好 $j$ 个正整数之和的合法方案总数。为了构建严谨的递推关系，我们可以依据拆分项的数值特征，将所有可能的方案进行完备的分类讨论：对于任何一组满足条件的拆分，其组成项要么包含至少一个 $1$ ，要么所有组成项均不小于 $2$ 。&lt;/p&gt;
&lt;p&gt;若当前拆分方案中包含至少一个 $1$ ，则可以将该项从该方案中剥离，这一操作使原问题直接等价转化为将 $i-1$ 划分为 $j-1$ 个正整数的子问题，从而在状态空间内建立了与 $dp[i-1][j-1]$ 的一一映射，即通过识别并剔除这一特定构成项，成功将问题规模约减。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5CDP-problems%5Cinteger-partition%5C%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%86%E5%9B%BE%E8%A7%A31.png&quot; alt=&quot;整数拆分图解&quot; /&gt;&lt;/p&gt;
&lt;p&gt;若拆分方案中的所有项均不小于 $2$ ，则可以将序列中的每个组成部分同时减去 $1$ ，通过这种整体平移的方式，使序列总和在保持拆分项数 $j$ 不变的情况下精确减少 $j$ 。这一操作将原问题等价转化为将 $i-j$ 拆分为 $j$ 个正整数的子问题，从而在逻辑层面上与 $dp[i-j][j]$ 建立起一一对应的映射关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5CDP-problems%5Cinteger-partition%5C%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%86%E5%9B%BE%E8%A7%A32.png&quot; alt=&quot;整数拆分图解&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于上述两类分类标准在数学逻辑上互斥且覆盖了所有拆分情形，根据组合计数的加法原理，我们可以推导出描述该状态演进的核心递推式：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i-1][j-1] + dp[i-j][j]
$$&lt;/p&gt;
&lt;p&gt;在自底向上的计算流程中，通过设定初始条件 $dp[0][0] = 1$ ，即可根据上述递推式依次计算出目标结果。这种在份数维度上构建状态的方法，巧妙地利用了拆分过程的结构特征，将原本复杂的组合计数问题转化为了条理清晰的状态转移，为分析此类整数拆分问题提供了一个直观的建模方式。&lt;/p&gt;
&lt;h3&gt;条件修改（一）&lt;/h3&gt;
&lt;p&gt;然而，当去除拆成恰好 $n$ 份的限制时，问题的核心逻辑发生了本质变化。此时求解任务变更为统计将整数 $S$ 拆分为任意数量的正整数之和的方案数。为了解决这种不再限制拆分项数的模型，我们可以定义二元状态 $dp[i][j]$ 表示在选取范围不超过 $j$ 的正整数前提下，拆分整数 $i$ 的合法方案总数。通过引入数值选取范围的单调性约束，该定义能够规避因元素排列顺序不同而导致的重复统计。&lt;/p&gt;
&lt;p&gt;为了构建严谨的递推关系，我们依据当前最大可选数值 $j$ 是否参与拆分，将所有方案划分为两个互斥的集合。若在构造总和为 $i$ 的序列时完全排除数值 $j$ ，则所有组成项必须在数值范围 ${1, 2, \ldots, j-1}$ 中选取，其对应的方案数贡献项为 $dp[i][j-1]$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5CDP-problems%5Cinteger-partition%5C%E6%97%A0%E4%BB%BD%E6%95%B0%E9%99%90%E5%88%B6%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%861.png&quot; alt=&quot;整数拆分图解&quot; /&gt;&lt;/p&gt;
&lt;p&gt;若拆分方案中至少包含一个数值 $j$ ，则我们可以通过从序列中剥离出一个 $j$ ，将原问题转化为拆分剩余数值 $i-j$ 的子问题。由于此模型允许元素重复使用，剥离操作后剩余部分的最大可选数值仍为 $j$ ，这一变换在原状态与子状态之间建立了直接的关联，其产生的方案数贡献量化为 $dp[i-j][j]$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5CDP-problems%5Cinteger-partition%5C%E6%97%A0%E4%BB%BD%E6%95%B0%E9%99%90%E5%88%B6%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%862.png&quot; alt=&quot;整数拆分图解&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于上述两类分类标准在数学逻辑上互斥且覆盖了所有拆分情形，根据组合计数的加法原理，我们可以推导出描述该状态演进的核心递推式：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i][j-1] + dp[i-j][j]
$$&lt;/p&gt;
&lt;p&gt;在自底向上的计算流程中，通过设定初始条件 $dp[0][0] = 1$ ，即可根据上述递推式依次计算出目标结果。这个模型在数学本质上等价于一个标准的完全背包计数问题，即每个整数 $1 \sim S$ 都可以被视为具备无限供给量的物品被多次选取，这与前文从份数维度出发的分析视角形成了鲜明的对比。&lt;/p&gt;
&lt;h3&gt;条件修改（二）&lt;/h3&gt;
&lt;p&gt;除了前述关于移除拆分项数约束的分析，我们也可以通过移除拆分结果必须为正整数的限制，转而允许各部分为非负整数。形式化地，若考虑满足 $a_1 + a_2 + \dots + a_n = S$ 且 $a_i \geq 0$ 的拆分情形，该问题逻辑上等价于将 $S$ 拆分为 &lt;strong&gt;至多 n 个正整数&lt;/strong&gt; 。这种等价性由非负拆分中的零值项与正整数拆分中的缺失项共同构建：通过剔除序列中所有的零值项，可以将非负序列转化为项数不大于 $n$ 的正整数序列；反之，若在正整数序列中补充适量的零值，即可将其补全为恰好包含 $n$ 项的非负序列。基于这一双向对应原理，该问题可以衍生出两种求解思路。&lt;/p&gt;
&lt;p&gt;第一种方法是沿用母题的动态规划状态进行求和，这种思路在保持原问题结构的同时，通过对所有可能的份数取值进行累加来获取目标方案数。若定义 $dp[i][j]$ 为将正整数 $i$ 拆分为恰好 $j$ 个正整数的方案数，由于至多 $n$ 份的拆分涵盖了从 $1$ 到 $n$ 所有可能的项数取值，我们只需将这些互斥的方案进行求和即可。最终的结果表达如下：&lt;/p&gt;
&lt;p&gt;$$
\text{Result} = \sum_{k=1}^{n} dp[S][k]
$$&lt;/p&gt;
&lt;p&gt;这种处理方式直接利用了既有的状态演进结果，通过对份数维度的全覆盖求和，在逻辑结构上展现了极高的直观性与实现便捷性，是最容易被理解的方案。&lt;/p&gt;
&lt;p&gt;第二种方法则利用了一个更为对称且精巧的代数平移关系，通过对数值边界的整体变换来简化计算维度。我们注意到，允许拆分项为 $0$ 的约束与每一份至少为 $1$ 的约束之间，本质上仅相差一个单位平移量。若对 $n$ 个拆分项统一施行加 $1$ 操作，即可将原方案 $\sum a_i = S$（其中 $a_i \geq 0$ ）转化为满足每一项至少为 $1$ 的新序列：&lt;/p&gt;
&lt;p&gt;$$
(a_1+1) + (a_2+1) + \ldots + (a_n+1) = S + n
$$&lt;/p&gt;
&lt;p&gt;此时，每一个新生成的项 $a_i+1$ 均严格满足大于等于 $1$ 的约束。这一变换建立了一个完美的数学对应：将 $S$ 拆分为恰好 $n$ 个非负整数的方案数，与将 $S+n$ 拆分为恰好 $n$ 个正整数的方案数相等。通过这种空间平移，原本涉及零元的边界限制被自然消解，从而将问题转化成最基础的正整数拆分模型。&lt;/p&gt;
&lt;h3&gt;组合数学视角&lt;/h3&gt;
&lt;p&gt;如果感觉上面的解法太过天马行空，那么可以回归生成函数（Generating Function）这一组合数学的基础工具，它往往能为状态设计提供更高维度的解释。为了解决整数拆分问题，我们需要追踪两个核心维度：总和等于 $S$（用变量 $x$ 的指数记录），拆分项数等于 $k$（用变量 $y$ 的指数记录）。&lt;/p&gt;
&lt;p&gt;对于每一个可用的正整数 $i$，决定使用它多少次，其代表的生成函数项是一个无穷级数：&lt;/p&gt;
&lt;p&gt;$$
f_i(x, y) = 1 + x^i y + (x^i y)^2 + (x^i y)^3 + \dots
$$&lt;/p&gt;
&lt;p&gt;利用等比级数求和公式，它可以化简为：&lt;/p&gt;
&lt;p&gt;$$
f_i(x, y) = \frac{1}{1 - x^i y}
$$&lt;/p&gt;
&lt;p&gt;为了得到完整问题的描述，我们将整数 $1 \sim S$ 的生成函数项相乘，得到完整问题的双元生成函数：&lt;/p&gt;
&lt;p&gt;$$
F(x, y) = \prod_{i=1}^{S} f_i(x, y) = \prod_{i=1}^{S} \frac{1}{1 - x^i y}
$$&lt;/p&gt;
&lt;p&gt;我们的最终目标，就是在这个代数式展开后，找到 $x^S y^k$ 项的系数。直接展开上述连乘式非常复杂，我们可以利用乘法分配律，一步步模拟括号相乘的过程。想象一下，我们有 $S$ 个括号，分别对应数字 $1, 2, \ldots, S$：&lt;/p&gt;
&lt;p&gt;$$
F(x, y) = (1 + x^1y + x^2y^2 + \dots) \times (1 + x^2y + x^4y^2 + \dots) \times \dots \times (1 + x^Sy + x^{2S}y^2 + \dots)
$$&lt;/p&gt;
&lt;p&gt;然后定义状态 $dp[i][j][k]$ 表示在处理完前 $i$ 个括号后，总和恰好为 $j$ 且恰好使用了 $k$ 个数字的方案数。当决策到第 $i$ 个数字（即第 $i$ 个括号）时，根据生成函数的乘法分配律，我们要么不选择数字 $i$ 让方案数继承自前 $i-1$ 个括号的结果，要么选择数字 $i$ 在已选过数字 $i$ 的状态基础上再累加。这恰好对应完全背包问题的递推逻辑，因此我们可以得到下面这个状态转移方程：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j][k] = dp[i-1][j][k] + dp[i][j-i][k-1]
$$&lt;/p&gt;
&lt;p&gt;生成函数为刻画组合约束提供了静态表征，而动态规划则是模拟该代数过程的有效手段。通过将生成函数的乘法结构映射为完全背包的递推形式，我们得以将抽象的代数逻辑转化为具体的求解方案。&lt;/p&gt;
&lt;h3&gt;费雷斯图视角&lt;/h3&gt;
&lt;p&gt;如果生成函数为整数拆分问题提供了代数层面的统一表达，那么费雷斯图（Ferrers Diagram）则赋予了拆分问题直观的几何灵魂。它不仅是一种可视化手段，更揭示了拆分问题中某种奇妙的 &lt;strong&gt;对称性&lt;/strong&gt; ，从而将原本复杂的组合计数问题转化为了更易于处理的等价形式。&lt;/p&gt;
&lt;p&gt;对于任意一个费雷斯图，如果沿着主对角线将其翻转（即行与列互换），可以得到一个新的费雷斯图，它对应着原整数 $S$ 的另一种拆分，这个过程被称为 &lt;strong&gt;共轭拆分&lt;/strong&gt; 。由于该变换在整数拆分集合上构成双射，因此计数保持不变。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5CDP-problems%5Cinteger-partition%5C%E8%B4%B9%E9%9B%B7%E6%96%AF%E5%8F%98%E6%8D%A2.png&quot; alt=&quot;费雷斯变换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;借助费雷斯图的对称性，我们能够建立起关键的等价关系：将 $S$ 拆分为恰好 $n$ 个部分的方案数，等价于将 $S$ 拆分为最大部分恰好为 $n$ 的方案数。为方便计算，我们利用&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/difference-idea/#%E7%AD%89%E5%BC%8F%E6%9D%A1%E4%BB%B6%E5%8F%98%E4%B8%BA%E4%B8%8D%E7%AD%89%E6%9D%A1%E4%BB%B6%E7%9B%B8%E5%85%B3%E9%A2%98%E7%9B%AE%E6%94%B6%E9%9B%86&quot;&gt;差分技巧&lt;/a&gt;将恰好型限制转化为至多型限制：&lt;/p&gt;
&lt;p&gt;$$
\text{count}(ans = n) = \text{count}(ans \leq n) - \text{count}(ans \leq n-1)
$$&lt;/p&gt;
&lt;p&gt;经过上述变形，该问题已经转化为&lt;a href=&quot;#%E6%9D%A1%E4%BB%B6%E4%BF%AE%E6%94%B9%E4%B8%80&quot;&gt;前文所述&lt;/a&gt;的 &lt;strong&gt;无拆分项数限制&lt;/strong&gt; 的整数拆分问题 ，只需将拆分项最大值限制为 $n$ 即可直接复用上述动态规划模型。若对该状态转移逻辑存在疑问，我们也可以从生成函数视角重新建模，原问题的费雷斯对称问题对应的生成函数为：&lt;/p&gt;
&lt;p&gt;$$
F(x) = \prod_{i=1}^{n} \frac{1}{1-x^i} = (1+x^1+x^2+\dots)(1+x^2+x^4+\dots)\dots(1+x^n+x^{2n}+\dots)
$$&lt;/p&gt;
&lt;p&gt;映射到 DP 空间，定义 $dp[i][j]$ 为利用前 $i$ 个正整数，构造总和为 $j$ 的方案数。递推时，依据是否选取数字 $i$ 分类讨论，具体的状态转移方程如下：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i-1][j] + dp[i][j-i]
$$&lt;/p&gt;
&lt;p&gt;最终答案的计算公式为：&lt;/p&gt;
&lt;p&gt;$$
\text{Result} = dp[n][S] - dp[n-1][S]
$$&lt;/p&gt;
&lt;p&gt;费雷斯变换巧妙地将 $y$ 维度的 &lt;strong&gt;份数约束&lt;/strong&gt; 转化为了 $x$ 维度上的 &lt;strong&gt;数值约束&lt;/strong&gt; ，结合容斥原理，我们将原本复杂的三维模型降维为高效的 $O(nS)$ 递推。这一过程体现了组合数学、代数推导与动态规划方法之间的深层统一。&lt;/p&gt;
&lt;p&gt;除了利用差分技巧对问题进行转化，我们还可以通过 &lt;strong&gt;平移视角&lt;/strong&gt; 进行等价变换。原题通过费雷斯变换得到的命题，实际上等价于将 $S-n$ 拆分为至多 $n$ 个非负整数部分这一命题。该变形的 &lt;strong&gt;直观理解&lt;/strong&gt; 如下：若要求拆分为恰好 $n$ 个正整数，即每个部分 $a_i \geq 1$ ，我们可以先为这 $n$ 个位置各预分配单位 $1$ 以满足限制，此时剩余总量为 $S-n$ 。这种平移视角与差分视角 &lt;strong&gt;殊途同归&lt;/strong&gt; ，但它为我们提供了一个更直接的计算公式：&lt;/p&gt;
&lt;p&gt;$$
\text{Result} = dp[n][S-n]
$$&lt;/p&gt;
&lt;p&gt;这里的 $dp[i][j]$ 依然沿用刚才的定义。对比之前的差分公式 $dp[n][S] - dp[n-1][S]$ ，我们可以意识到这两个式子在代数上是恒等的，移项变形后可以发现恰好为背包 DP 的转移公式：&lt;/p&gt;
&lt;p&gt;$$
dp[n][S] = dp[n - 1][S] + dp[n][S-n]
$$&lt;/p&gt;
&lt;p&gt;这种变形的精妙之处在于，它通过对数值空间的整体平移，完全规避了容斥原理中繁琐的加减抵消过程。它巧妙地将 “恰好 $n$ 份” 这一带有强制性的组合硬约束，直接转化为了一个规模更小的数值受限拆分问题，使得状态转移更加简洁高效。&lt;/p&gt;
&lt;h3&gt;五边形数视角&lt;/h3&gt;
&lt;p&gt;经过上述几种方法的探讨，我们已对整数拆分问题建立起系统认知。然而，针对&lt;a href=&quot;#%E6%9D%A1%E4%BB%B6%E4%BF%AE%E6%94%B9%E4%B8%80&quot;&gt;无拆分项数限制&lt;/a&gt;的整数拆分问题，我们还可以引入数学史上极具美感的五边形数定理（Pentagonal Number Theorem）。该定理不仅揭示了拆分函数内部极其深层的级数结构，更利用其独特的稀疏性，将复杂的动态规划进一步优化为 &lt;strong&gt;极高效的线性递推&lt;/strong&gt; ，为理解问题的本质提供了更高维度的代数视角。&lt;/p&gt;
&lt;p&gt;在深入探讨之前，我们需要明确 &lt;strong&gt;五边形数&lt;/strong&gt; 的定义：五边形数是一种可以排列成五边形的数，与三角形数和正方形数类似。从几何构造上看，五边形数 $G_k$ 是通过中心点向外逐层扩展而成的，其通项公式为：&lt;/p&gt;
&lt;p&gt;$$
G_k = \frac{k(3k-1)}{2}
$$&lt;/p&gt;
&lt;p&gt;这里 $k$ 可以是正整数（对应几何意义的五边形数）或负整数（对应泛化的五边形数）。&lt;/p&gt;
&lt;p&gt;之所以整数拆分问题能与五边形数建立深层联结，其核心根源在于欧拉在探究拆分函数 $p(n)$ 的生成函数性质时，对该函数的代数倒数（即著名的 &lt;strong&gt;欧拉函数&lt;/strong&gt; ）进行了一系列极具启发性的分析。欧拉函数在形式上被定义为：&lt;/p&gt;
&lt;p&gt;$$
\phi(x) = \prod_{m=1}^{\infty} (1-x^m) = (1-x)(1-x^2)(1-x^3)\dots
$$&lt;/p&gt;
&lt;p&gt;这种连乘积的形式看起来极其复杂，但令人震惊的是，将其展开后，大多数项的系数竟然能相互抵消为 $0$ 。只有当指数恰好为五边形数时，系数才不为 $0$ ，且仅为 $1$ 或 $-1$ 。具体展开式即为五边形数定理：&lt;/p&gt;
&lt;p&gt;$$
\phi(x) = \prod_{m=1}^{\infty} (1-x^m) = \sum_{k=-\infty}^{\infty} (-1)^k x^{\frac{k(3k-1)}{2}}
$$&lt;/p&gt;
&lt;p&gt;这一结构将无穷乘积转化为 &lt;strong&gt;稀疏级数&lt;/strong&gt; ，为研究 $p(n)$ 提供了本质路径。&lt;/p&gt;
&lt;p&gt;基于五边形数定理，计算特定整数 $n$ 的拆分方案数 $p(n)$ 不再需要枚举所有的生成函数项，而是可以通过一个极快且结构对称的递推公式来实现。利用欧拉函数与拆分函数生成函数的倒数关系 $\phi(x) P(x) = 1$ ，也就是：&lt;/p&gt;
&lt;p&gt;$$
\left( \sum_{k=-\infty}^{\infty} (-1)^k x^{G_k} \right) \left( \sum_{n=0}^{\infty} p(n)x^n \right) = 1
$$&lt;/p&gt;
&lt;p&gt;我们可以将这一恒等式转化为关于 $p(n)$ 的线性递推关系。通过将 $\phi(x)$ 的级数表示代入，经过移项和整理，我们便得到了著名的五边形数递推公式：&lt;/p&gt;
&lt;p&gt;$$
p(n) = \sum_{k \neq 0, G_k \leq n} (-1)^{k-1} p(n-G_k)
$$&lt;/p&gt;
&lt;p&gt;根据此公式，计算 $p(n)$ 只需要累加之前计算过的 $p(n-G_k)$ 的值，其中 $G_k$ 是五边形数。这不仅降低了计算的维度，还使得计算复杂度大幅下降，在实际应用中表现出极高的计算效率。&lt;/p&gt;
&lt;h2&gt;整数的划分问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1025&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;将整数 $n$ 分成 $k$ 个正整数之和，且要求这 $k$ 个正整数之和为 $n$ ，即 $n = a_1 + a_2 + \ldots + a_k$ 。&lt;/p&gt;
&lt;p&gt;同时要求满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$1 \leq a_1 \leq a_2 \leq \ldots \leq a_k$（非递减序列，以避免重复计算）。&lt;/li&gt;
&lt;li&gt;$k$ 个正整数之和为 $n$ 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请问有多少种不同的划分方案？&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$6 \leq n \leq 200$&lt;/li&gt;
&lt;li&gt;$2 \leq k \leq 6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示不同的划分方案数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/lyaba9l8&quot;&gt;【Luogu 博客】浅谈整数分拆问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://mem.ac/oi/algorithm/rotational-symmetry/&quot;&gt;【memset0】五边形数定理学习笔记&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】优先队列的应用</title><link>https://xingguang641.com/posts/acm/acm-note/priority-queue/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/priority-queue/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Fri, 13 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;最优状态维护问题&lt;/h1&gt;
&lt;p&gt;许多算法题目的目标是要求 &lt;strong&gt;输出前 K 个最优结果&lt;/strong&gt; ，这类问题的核心挑战在于，候选状态的总数可能呈指数级增长，但真正具备参考价值的仅为指定的 $K$ 个结果。因此，与其在生成全部结果后再进行全局排序，不如通过 &lt;strong&gt;优先队列&lt;/strong&gt; 实时更新并维护当前最优的候选。这种策略有效地将计算聚集在最有潜力的状态上，从根本上避免了无效的枚举，从而显著降低了算法的时间复杂度。&lt;/p&gt;
&lt;p&gt;在实际应用中，最优状态维护问题通常与 &lt;strong&gt;状态扩展&lt;/strong&gt; 和 &lt;strong&gt;搜索剪枝&lt;/strong&gt; 深度结合。在这种寻优模式下，堆顶始终维持着当前全局最优的状态；当该状态被取出后，算法通过精心设计的变换逻辑生成其后续候选状态并重新入堆。通过合理的算法设计，我们能够确保从最优状态扩展出的新状态依然具有较强的竞争力，从而在 &lt;strong&gt;优中选优&lt;/strong&gt; 的过程中自然实现剪枝。通过这种方式，算法能够逐步提取前 $K$ 个结果。&lt;/p&gt;
&lt;h2&gt;数组的第K大和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-the-k-sum-of-an-array/description&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个下标从 $0$ 开始、长度为 $n$ 的整数数组 &lt;code&gt;nums&lt;/code&gt; ，和两个整数 &lt;code&gt;k&lt;/code&gt; 。你需要从数组中找出一个子序列，使得该子序列内元素的 &lt;strong&gt;和&lt;/strong&gt; 为所有子序列和中 &lt;strong&gt;第 k 大&lt;/strong&gt; 的一个。&lt;/p&gt;
&lt;p&gt;返回该数组的 &lt;strong&gt;第 k 大子序列和&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;注意：子序列是指从数组中删除一些元素（也可以不删除）后剩余元素组成的数组。空子序列的和定义为 $0$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == nums.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq \min(2000, 2^n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示第 $k$ 大的子序列和。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
2 4 -2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 5
1 -2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;我们首先考虑该问题的 &lt;strong&gt;简化版本&lt;/strong&gt;：给定 $n$ 个非递减的非负整数序列 $a_0, a_1, \ldots, a_{n-1}$ ，找出其中第 $k$ 个最小的子序列和。在这个条件下，最小的和显然是空序列产生的 $0$（即 $k=1$ 时的答案）。对于 $k &amp;gt; 1$ 的情况，核心在于如何有序地产出后续所有的和。我们引入状态 $(t, i)$ 表示一个以 $a_i$ 为最后一个元素且总和为 $t$ 的子序列，并利用 &lt;strong&gt;最小堆&lt;/strong&gt; 来动态维护这些状态。初始化时，堆中仅包含序列中最小的元素 $(a_0, 0)$ 。&lt;/p&gt;
&lt;p&gt;为了确保探索过程有序且完备，每当我们从堆顶取出当前的最小状态 $(t, i)$ 时，需要执行两项关键的转移操作。第一是将 $a_{i+1}$ &lt;strong&gt;拼接&lt;/strong&gt; 到当前子序列之后，生成新状态 $(t + a_{i+1}, i + 1)$ ，代表子序列长度的增加。第二是将当前子序列中的末尾元素 $a_i$ &lt;strong&gt;替换&lt;/strong&gt; 为更大的 $a_{i+1}$，生成状态 $(t - a_i + a_{i+1}, i + 1)$ 。这种包含与替换的策略保证了每一次从堆顶弹出的元素都是当前全局尚未被发现的最小值。&lt;/p&gt;
&lt;p&gt;通过这种方式，当我们第 $k-1$ 次从堆中取出元素时，所得到的 $t$ 就是序列中第 $k$ 个最小的子序列和。这种构造逻辑实际上是在一颗隐形的状态树上进行层序遍历，它利用了数组原本的有序性，避免了对 $2^n$ 空间的盲目搜索。&lt;/p&gt;
&lt;p&gt;回到原问题中包含负数的情况，我们可以发现原序列中所有正数之和 $maxSum$ 构成了该数组能够达到的全局最大子序列和。任何其他子序列相对于这个最大值的降低，实质上都是由于两种行为导致的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要么是放弃了某个原本选中的正数 $x$ ，损失了 $|x|$&lt;/li&gt;
&lt;li&gt;要么是加入了一个原本未选的负数 $y$ ，损失了 $|y|$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这一观察，无论原数正负，它们对总和造成的破坏程度都整齐划一地由其 &lt;strong&gt;绝对值&lt;/strong&gt; 来衡量。我们只需要将原序列的所有元素统一取绝对值并从小到大排序，就将其完美转化为了上述的简化模型。此时，原问题的第 $k$ 大子序列和就等价于用全局最大和 $maxSum$ 减去绝对值序列中的第 $k-1$ 小子序列和。这种巧妙的对称性转化，避开了对正负数排列组合的复杂讨论，利用最小堆在 $O(k \log k)$ 的复杂度内精准锁定了目标结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
int n, k; ll sum = 0;

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; k;
    vector&amp;lt;int&amp;gt; nums(n);
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; nums[i];
        if (nums[i] &amp;gt; 0) sum += nums[i];
        else nums[i] = -nums[i];
    }

    sort(nums.begin(), nums.end());
    if (k == 1) {
        cout &amp;lt;&amp;lt; sum &amp;lt;&amp;lt; endl;
        return 0;
    }

    priority_queue&amp;lt;
        pair&amp;lt;ll, int&amp;gt;,
        vector&amp;lt;pair&amp;lt;ll, int&amp;gt;&amp;gt;,
        greater&amp;lt;pair&amp;lt;ll, int&amp;gt;&amp;gt;
    &amp;gt; pq;
    pq.push({(ll)nums[0], 0});
    ll min_loss = 0;
    for (int i = 0; i &amp;lt; k - 1; i++) {
        auto [loss, idx] = pq.top(); pq.pop();
        if (idx + 1 &amp;lt; n) {
            pq.push({loss + nums[idx + 1], idx + 1});
            pq.push({loss + nums[idx + 1] - nums[idx], idx + 1});
        }
        min_loss = loss;
    }

    cout &amp;lt;&amp;lt; sum - min_loss &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最小的包含区间&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/smallest-range-covering-elements-from-k-lists/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;题目相关拓展&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimize-deviation-in-array/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;序列的合并问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1631&quot;&gt;题目连接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有两个长度都是 $N$ 的正整数序列 $A$ 和 $B$ ，在 $A$ 和 $B$ 中各取一个数相加可以得到 $N^2$ 个和，求这 $N^2$ 个和中 &lt;strong&gt;最小的 N 个&lt;/strong&gt; 值。&lt;/p&gt;
&lt;p&gt;请注意：本题要求按 &lt;strong&gt;从小到大&lt;/strong&gt; 的顺序输出前 $N$ 个最小的和。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == A.length == B.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A[i], B[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n$&lt;/li&gt;
&lt;li&gt;序列 $A$ 和 $B$ 均已按 &lt;strong&gt;升序&lt;/strong&gt; 排列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示序列 $A$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $N$ 个整数，表示序列 $B$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_N$&lt;/p&gt;
&lt;p&gt;$B_1 \quad B_2 \quad \ldots \quad B_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含一行，包含 $N$ 个整数，两两之间用空格隔开，表示最小的 $N$ 个和。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 2 3
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题目的核心在于如何从 $N^2$ 个可能的结果中，高效地筛选出最小的 $N$ 个值。由于给定的两个序列 $A$ 和 $B$ 均已按 &lt;strong&gt;升序排列&lt;/strong&gt; ，我们可以将这个庞大的组合空间看作是 $N$ 条相互独立的 &lt;strong&gt;有序链表&lt;/strong&gt; 。具体来说，我们可以固定序列 $A$ 中的每一个元素 $a_i$ ，将其与序列 $B$ 中的所有元素相加，得到序列 ${a_i + b_0, a_i + b_1, \ldots, a_i + b_{N-1}}$ 。由于 $B$ 是有序的，这 $N$ 条链表每一条内部也必然是单调递增的。&lt;/p&gt;
&lt;p&gt;为了在不遍历整个 $N^2$ 空间的前提下获得全局最优解，我们可以利用 &lt;strong&gt;最小堆&lt;/strong&gt; 来维护这 $N$ 条链表当前的边界。初始时，我们将每一条链表的第一个元素（即 $a_i + b_0$ ）连同其在 $A$ 和 $B$ 中的索引信息全部推入堆中。此时，堆顶元素即为全场最小的初始和。随后，我们进行 $N$ 次提取操作：每当从堆顶弹出当前的最小值时，立即通过索引信息找到该元素所属链表的 &lt;strong&gt;下一个候选者&lt;/strong&gt; 并将其补充进堆。&lt;/p&gt;
&lt;p&gt;这种策略的精妙之处在于，它通过 &lt;strong&gt;局部有序性&lt;/strong&gt; 成功锁定了搜索的边界，使得堆的大小始终维持在 $O(N)$ 。在每一次弹出最小值后，我们只需要关注那条刚刚被消耗掉一个元素的链表，而不需要去查找其他链表深处的元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAX = 1e5 + 100;
int a[MAX], b[MAX];

int main(){
    int N; cin &amp;gt;&amp;gt; N;
    for (int i = 0; i &amp;lt; N; i++) cin &amp;gt;&amp;gt; a[i];
    for (int i = 0; i &amp;lt; N; i++) cin &amp;gt;&amp;gt; b[i];

    priority_queue&amp;lt;
        pair&amp;lt;int, pair&amp;lt;int, int&amp;gt;&amp;gt;, 
        vector&amp;lt;pair&amp;lt;int, pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt;, 
        greater&amp;lt;pair&amp;lt;int, pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt;
    &amp;gt; pq;
    for (int i = 0; i &amp;lt; N; i++){
        pq.push({a[i] + b[0], {i, 0}});
    }
    for (int i = 0; i &amp;lt; N; i++){
        auto [cur, idx] = pq.top(); pq.pop();
        cout &amp;lt;&amp;lt; cur &amp;lt;&amp;lt; &quot; &quot;;
        if (idx.second + 1 &amp;lt; N)
            pq.push({a[idx.first] + b[idx.second + 1],
            {idx.first, idx.second + 1}});
    } cout &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组建机器人奶牛&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2541&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;Bessie 需要建造 $K$ 头不同的机器人奶牛。每头机器人奶牛有 $N$ 个位置需要安装微控制器。对于每个位置 $i$ ，都有 $M_i$ 个备选的微控制器模型，每个模型都有对应的成本。&lt;/p&gt;
&lt;p&gt;你需要从每个位置的备选模型中各选出一个，组成一头完整的机器人。由于每头机器人的微控制器组合必须是唯一的，你的目标是选出总成本最小的 $K$ 种不同组合，并计算这 &lt;strong&gt;K 套方案的总成本之和&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq K \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq M_i \leq 10$&lt;/li&gt;
&lt;li&gt;$1 \leq P_{i,j} \leq 10^8$&lt;/li&gt;
&lt;li&gt;保证方案总数不少于 $K$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $K$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $N$ 行，第一个整数为该位置的模型数量 $M_i$ ，随后 $M_i$ 个整数表示该位置各个模型的成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad K$&lt;/p&gt;
&lt;p&gt;$M_1 \quad P_{1,1} \quad P_{1,2} \quad \ldots \quad P_{1,M_1}$&lt;/p&gt;
&lt;p&gt;$M_2 \quad P_{2,1} \quad P_{2,2} \quad \ldots \quad P_{2,M_2}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$M_N \quad P_{N,1} \quad P_{N,2} \quad \ldots \quad P_{N,M_N}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示建造 $K$ 头不同机器人奶牛的最小总成本之和。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 3
2 1 10
2 5 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;状态转移设计，如何设计有限的状态转移得到所有状态是这类问题的难点&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;区间最优调度问题&lt;/h1&gt;
&lt;p&gt;排序其中一端，然后维护另一端&lt;/p&gt;
&lt;h2&gt;会议室安排问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.doocs.org/lc/253/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这题就是经典的区间调度问题&lt;/p&gt;
&lt;h2&gt;最大的会议数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-events-that-can-be-attended/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个数组 $events$ ，其中 $events[i] = [startDay_i, endDay_i]$ ，表示会议 $i$ 开始于 $startDay_i$ ，结束于 $endDay_i$ 。你可以在 $startDay_i \leq d \leq endDay_i$ 中的任意一天 $d$ 参加会议 $i$ 。每场会议你只需参加 &lt;strong&gt;一天&lt;/strong&gt; 就可以算作已参加。每天你最多只能参加一场会议。&lt;/p&gt;
&lt;p&gt;请返回你能参加的最大会议数目。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq events.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$events[i].length == 2$&lt;/li&gt;
&lt;li&gt;$1 \leq startDay_i \leq endDay_i \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示会议的总数。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含两个整数，分别表示第 $i$ 场会议的开始时间 $startDay_i$ 和结束时间 $endDay_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$startDay_1 \quad endDay_1$&lt;/p&gt;
&lt;p&gt;$startDay_2 \quad endDay_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$startDay_n \quad endDay_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示你能参加的最大会议数目。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 2
2 3
3 4
1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 4
4 4
2 2
3 4
1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这题就是经典的区间调度问题（排序两段都可以做，记得看灵神题解）&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;数据流中位数问题&lt;/h1&gt;
&lt;p&gt;数据流中位数问题的核心挑战在于数据以连续流的形式输入，并且要求系统在 &lt;strong&gt;每次插入操作后定位序列的中心位置&lt;/strong&gt; 。如果采用全局排序的方式，计算成本会随着数据量的累积而迅速上升，难以满足实时处理的需求。中位数的数学性质在于它将有序序列划分为较小和较大的两个等分区间，其数值由左侧区间的最大值或右侧区间的最小值导出。因此，问题的关键不在于维护全局的有序性，而在于如何高效地动态维护这两个半区的边界。&lt;/p&gt;
&lt;p&gt;实现这一目标的标准方案是构建双堆结构：利用一个 &lt;strong&gt;最大堆&lt;/strong&gt; 管理较小的一半数据，确保堆顶始终锁定左半部分的边界最大值；同时利用一个 &lt;strong&gt;最小堆&lt;/strong&gt; 管理较大的一半数据，使堆顶始终锁定右半部分的边界最小值。每当新数据进入系统，程序会根据数值大小将其分流至对应的堆中，并实时通过跨堆移动来平衡两者的规模，确保其元素数量之差不超过 1。在这种架构下，中位数始终处于两个堆顶的覆盖范围内，从而将查询效率优化至 $O(1)$ ，维护效率保持在 $O(\log n)$ 。&lt;/p&gt;
&lt;p&gt;这种方法的本质是利用优先队列的局部有序性，对数据进行 &lt;strong&gt;分组维护并暴露关键的边界元素&lt;/strong&gt; 。在双堆架构中，系统不再消耗资源去处理每一个元素的绝对排名，而是将算力集中在能够决定中位数的关键节点上。这种动态维护序列关键位置的策略，彻底规避了频繁排序带来的性能瓶颈，使得处理大规模实时数据流变得轻量且高效。&lt;/p&gt;
&lt;h2&gt;滑动窗口中位数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/sliding-window-median/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;中位数是有序序列中间的数。如果序列的长度是偶数，中位数则是中间两个数的平均值。&lt;/p&gt;
&lt;p&gt;给定一个长度为 $N$ 的数组 $nums$ 和一个窗口大小 $k$ ，有一个大小为 $k$ 的窗口从数组的最左侧移动到最右侧。窗口每次向右移动一位。你的目标是找出每次窗口移动后，窗口内 $k$ 个数字的中位数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq k \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-2^{31} \leq nums[i] \leq 2^{31} - 1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $nums$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$num_1 \quad num_2 \quad \ldots \quad num_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行浮点数（保留五位小数），每个数之间用空格隔开，表示每个窗口的中位数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8 3
1 3 -1 -3 5 3 6 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1.00000 -1.00000 -1.00000 3.00000 5.00000 6.00000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;滑动窗口中位数问题的核心挑战在于，如何在窗口沿着序列移动的过程中，实时且高效地求解中位数。若每次移动窗口都对内部的 $k$ 个元素进行排序，时间复杂度将达到 $O(N \cdot k \log k)$ ，在 $10^5$ 的数据规模下会导致计算成本过高。因此，解题的关键在于构建一种能够动态维护有序性的机制，支持在增删元素时以极低成本定位中间值，从而避免对整个窗口的重复操作。&lt;/p&gt;
&lt;p&gt;为了实现这一目标，可以使用对顶堆的设计：利用 &lt;strong&gt;最大堆&lt;/strong&gt; 维护较小的一半数据，利用 &lt;strong&gt;最小堆&lt;/strong&gt; 维护较大的一半数据，确保中位数始终处于两个堆顶的交界处。当窗口沿着序列移动时，只需将新滑入的元素归入对应的堆，同时将滑出的元素从相应的堆中移除，并维持两个堆的规模平衡。通过这种双堆对顶策略，窗口中位数可在 $O(1)$ 时间内由堆顶元素直接计算得出，从而将单次滑动后的更新效率优化至 $O(\log k)$ ，确保算法在处理大规模动态数据流时依然高效且稳定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
multiset&amp;lt;int&amp;gt; leftset, rightset;

void leftToRight() {
    if (leftset.empty()) return;
    rightset.insert(*leftset.rbegin());
    leftset.erase(prev(leftset.end()));
}

void rightToleft() {
    if (rightset.empty()) return;
    leftset.insert(*rightset.begin());
    rightset.erase(rightset.begin());
}

void balance() {
    if (leftset.size() &amp;gt; rightset.size() + 1)
        leftToRight();
    if (rightset.size() &amp;gt; leftset.size())
        rightToleft();
}

int main() {
    int N, k; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; k;
    vector&amp;lt;int&amp;gt; nums(N);
    for (int i = 0; i &amp;lt; N; i++) {
        cin &amp;gt;&amp;gt; nums[i];
    }

    for (int i = 0; i &amp;lt; N; i++) {
        if (leftset.empty() || nums[i] &amp;lt;= *leftset.rbegin())
            leftset.insert(nums[i]);
        else 
            rightset.insert(nums[i]);
        
        if (i &amp;gt;= k) {
            int out_val = nums[i - k];
            auto it = leftset.find(out_val);
            if (it != leftset.end()) 
                leftset.erase(it);
            else 
                rightset.erase(rightset.find(out_val));
        }

        balance();

        if (i &amp;gt;= k - 1) {
            double res;
            if (k % 2 == 1) {
                res = (double)*leftset.rbegin();
            } else {
                res = ((double)*leftset.rbegin() + *rightset.begin()) / 2.0;
            }
            cout &amp;lt;&amp;lt; fixed &amp;lt;&amp;lt; setprecision(1) &amp;lt;&amp;lt; res &amp;lt;&amp;lt; (i == N - 1 ? &quot;&quot; : &quot; &quot;);
        }
    }
    cout &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;反悔贪心策略问题&lt;/h1&gt;
&lt;p&gt;在传统的算法逻辑中，&lt;strong&gt;贪心策略&lt;/strong&gt; 往往被视为一种一次性决策，即每一步都盲目追求当前状态下的局部最优。然而，许多复杂问题的全局最优解并不由局部最优简单累加而成，这就导致常规贪心容易陷入误区。&lt;strong&gt;反悔贪心&lt;/strong&gt; 的引入，本质上是为这种僵化的决策机制注入了 &lt;strong&gt;动态修正&lt;/strong&gt; 的能力。它不再强求每一步都绝对正确，而是允许算法先执行一次 &lt;strong&gt;假贪心&lt;/strong&gt; ，在后续过程中根据全局利益的变化，灵活地收回并替换之前的决策。&lt;/p&gt;
&lt;p&gt;反悔贪心的巧妙之处在于它构建了一个允许推倒重来的 &lt;strong&gt;反馈回路&lt;/strong&gt; 。通过引入优先队列等数据结构，算法能够量化当前决策的 &lt;strong&gt;反悔成本&lt;/strong&gt; 与后续候选方案的 &lt;strong&gt;边际收益&lt;/strong&gt; 。当新出现的全局选择优于历史操作时，算法利用这种差值度量实现决策的 &lt;strong&gt;动态回溯与替换&lt;/strong&gt; 。本质上，反悔贪心将局部最优转化为一种可调控的 &lt;strong&gt;状态空间搜索&lt;/strong&gt; ，通过对决策增量的持续维护，使得算法在执行过程中能够不断自我纠错，从而在低时间复杂度下逼近 &lt;strong&gt;全局最优&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;工作的调度问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2949&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $N$ 项任务，每项任务需要花费 $1$ 个单位时间来完成。对于第 $i$ 项任务，它有一个截止时间 $d_i$ 和一个完成该任务后可以获得的价值 $p_i$ 。每一时刻只能完成一项任务。&lt;/p&gt;
&lt;p&gt;你的目标是合理安排任务的执行顺序，使得在所有截止时间之前完成的任务总价值最大。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq d_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq p_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示任务的总数。&lt;/li&gt;
&lt;li&gt;接下来的 $N$ 行，每行包含两个整数 $d_i$ 和 $p_i$ ，分别表示第 $i$ 项任务的截止时间和价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$d_1 \quad p_1$&lt;/p&gt;
&lt;p&gt;$d_2 \quad p_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$d_N \quad p_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能够获得的最大总价值。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2 10
1 5
1 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;17
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;股票收益最大化&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF865D&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你预测了未来 $N$ 天某只股票的价格。在第 $i$ 天，股票的价格为 $c_i$ 。&lt;/p&gt;
&lt;p&gt;每天你可以执行以下三种操作之一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;买入&lt;/strong&gt;：花费 $c_i$ 的代价买入一股股票。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卖出&lt;/strong&gt;：将手中已持有的一股股票卖出，获得 $c_i$ 的收益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;观望&lt;/strong&gt;：不进行任何买入或卖出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设你初始资金无限，且不限制持有股票的数量。你的目标是通过合理的操作，使得 $N$ 天后的总利润最大化。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 3 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq c_i \leq 10^6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示天数。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数 $c_1, c_2, \ldots, c_N$ ，分别表示第 $i$ 天的股票价格。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$c_1 \quad c_2 \quad \ldots \quad c_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能够获得的最大总利润。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
10 5 4 7 9 12 6 2 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;20
3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 2 3 8 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;41
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这是一道维护候选项的题，我们可以引入反悔状态&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/heap-problems/greedy-algorithm/&quot;&gt;【ACM 算法题单】贪心算法相关问题&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】子数组最大累加和问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-problems/maximum-subarray-sum/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-problems/maximum-subarray-sum/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单区间累加和问题&lt;/h1&gt;
&lt;p&gt;在动态规划的视角下，处理连续子数组问题需要 &lt;strong&gt;严格的逻辑安排&lt;/strong&gt; 。由于题目要求元素在空间上必须紧密相连，动态规划的重点不在于全局搜索，而在于如何让递推过程始终受限于这种连续性。与处理子序列问题时那种相对自由的状态转移不同，连续子数组问题要求每一个状态都必须明确地归属于以当前元素为结尾的区间。这种对 &lt;strong&gt;终点位置的严格限定&lt;/strong&gt; ，有效地将复杂的整体问题转化为对每个元素位置的单点抉择，从而消除了逻辑上的歧义，确保了整个推导过程的可靠性与准确性。&lt;/p&gt;
&lt;p&gt;针对此类问题，动态规划具体的状态设计是将 $dp[i]$ 定义为以第 $i$ 个位置为区间终点的最优解。在算法执行到每一个节点时，核心的逻辑在于 &lt;strong&gt;决策是否延续前序区间的最优积累&lt;/strong&gt; ，即通过分析当前节点独立构成新区间的可能性，或者将其并入现有区间所带来的数值变化，来确定当前状态的最优值。这种 &lt;strong&gt;逐点推进的策略&lt;/strong&gt; 清晰地展现了局部最优解如何通过严格的路径约束，在不断迭代中构建出全局的最优结果。&lt;/p&gt;
&lt;h2&gt;子数组最大总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-subarray&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。&lt;/p&gt;
&lt;p&gt;子数组是数组中的一个连续部分。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-10^4 \leq nums[i] \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1, nums_2, \ldots, nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
-2 1 -3 4 -1 2 1 -5 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;处理连续子数组问题的核心在于引入 “以当前位置结尾” 这一约束，将原本发散的区间搜索转化为有序的动态规划递推。通过定义 $dp[i]$ 为以第 $i$ 个元素为终点的最大子数组和，我们将复杂的全局最优解问题简化为局部的逻辑决策，即判断是承接前序序列以累加收益，还是舍弃前序以当前元素为起点重新构建。&lt;/p&gt;
&lt;p&gt;其核心递推关系简洁地表达为：&lt;/p&gt;
&lt;p&gt;$$
dp[i] = \max(nums[i], dp[i-1] + nums[i])
$$&lt;/p&gt;
&lt;p&gt;这种建模技巧的本质是用局部结尾的确定性来换取状态转移的极简性。通过一次线性扫描，我们在每一个递推步中捕获局部最优，并在遍历过程中维护全局最大值，从而在 $O(n)$ 的时间复杂度内完成对连续性约束问题的求解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 100;
int a[MAXN]; ll dp[MAXN];

int main() {
    int n; cin &amp;gt;&amp;gt; n;
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; a[i];
    }

    dp[0] = a[0]; ll ans = a[0];
    for (int i = 1; i &amp;lt; n; i++) {
        dp[i] = max((ll)a[i], dp[i - 1] + a[i]);
        ans = max(ans, dp[i]);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环数组最大总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-sum-circular-subarray/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 &lt;code&gt;n&lt;/code&gt; 的 &lt;strong&gt;环形整数数组&lt;/strong&gt; &lt;code&gt;nums&lt;/code&gt; ，请你找出该数组中具有最大和的 &lt;strong&gt;非空子数组&lt;/strong&gt; ，并返回其最大和。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;环形数组&lt;/strong&gt; 意味着数组的末端将会与开头相连呈环状。形式上，&lt;code&gt;nums[i]&lt;/code&gt; 的下一个元素是 &lt;code&gt;nums[(i + 1) % n]&lt;/code&gt; ，而 &lt;code&gt;nums[i]&lt;/code&gt; 的前一个元素是 &lt;code&gt;nums[(i - 1 + n) % n]&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == nums.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 3 \times 10^4$&lt;/li&gt;
&lt;li&gt;$-3 \times 10^4 \leq nums[i] \leq 3 \times 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示环形数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1, nums_2, \ldots, nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 -2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
5 -3 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;针对环形数组，常见的处理策略通常分为两类：一种是 &lt;strong&gt;倍增数组法&lt;/strong&gt; ，通过构造长度为 $2n$ 的数组将环形结构展平为线性序列，并使用长度为 $n$ 的定长滑动窗口规定数组范围，从而避免决策中对同一元素的重复选择。虽然该方法直观易于实现，但其代价在于需要进行 $n$ 次完整滑动窗口的循环操作，导致总时间复杂度增加，在面对大规模数据时效率极低。另一种更为高效的策略是 &lt;strong&gt;分类讨论法&lt;/strong&gt; ，其核心在于根据子数组是否跨越数组边界，将问题拆解为两个对偶的线性子问题。&lt;/p&gt;
&lt;p&gt;对于 &lt;strong&gt;不跨越边界&lt;/strong&gt; 的情况，子数组完全包含在原数组内部，其求解等价于标准的线性子数组最大累加和。通过状态转移方程 $dp_{max}[i] = \max(a[i], dp_{max}[i-1] + a[i])$ ，可以计算以每个位置结尾的最大增益。对于 &lt;strong&gt;跨越边界&lt;/strong&gt; 的情况，最大子数组由原数组的前缀与后缀拼接而成，其逻辑等价于从总和 $TotalSum$ 中减去中间的一段连续最小子数组。因此，求解跨越边界的最大和，转化为求解线性数组的最小累加和这一对偶问题，通过 $dp_{min}[i] = \min(a[i], dp_{min}[i-1] + a[i])$ 即可同步维护。&lt;/p&gt;
&lt;p&gt;最终的全局最优解在 $\max(dp_{max})$ 与 $TotalSum - \min(dp_{min})$ 之间产生。这种处理方式规避了显式的环形遍历，通过同步维护最大与最小状态，在 $O(n)$ 时间复杂度内实现了对全解空间的覆盖。需要注意的是，当数组元素全部为负值时，$TotalSum$ 与最小累加和相等，此时对应的补集为空集，应直接取 $\max(dp_{max})$ 作为结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 100;
int a[MAXN];
ll dp_max[MAXN], dp_min[MAXN];

int main() {
    int n; cin &amp;gt;&amp;gt; n;
    ll total_sum = 0;
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; a[i];
        total_sum += a[i];
    }

    dp_max[0] = dp_min[0] = a[0];
    ll max_ans = a[0];
    ll min_ans = a[0];
    for (int i = 1; i &amp;lt; n; i++) {
        dp_max[i] = max((ll)a[i], dp_max[i - 1] + a[i]);
        max_ans = max(max_ans, dp_max[i]);

        dp_min[i] = min((ll)a[i], dp_min[i - 1] + a[i]);
        min_ans = min(min_ans, dp_min[i]);
    }

    if (max_ans &amp;lt; 0) {
        cout &amp;lt;&amp;lt; max_ans &amp;lt;&amp;lt; endl;
    } else {
        cout &amp;lt;&amp;lt; max(max_ans, total_sum - min_ans) &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;子数组最大乘积&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-product-subarray/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; ，请你找出数组中乘积最大的非空连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。&lt;/p&gt;
&lt;p&gt;测试用例的答案是一个 &lt;strong&gt;32 位&lt;/strong&gt; 整数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 2 \times 10^4$&lt;/li&gt;
&lt;li&gt;$-10 \leq nums[i] \leq 10$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums&lt;/code&gt; 的任意前缀或后缀的乘积都在 &lt;strong&gt;32 位&lt;/strong&gt; 整数范围内&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1, nums_2, \ldots, nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
2 3 -2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
-2 0 -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题与最大子数组累加和的思路如出一辙，核心逻辑并没有发生本质改变。我们依然是在维护一个以当前位置结尾的最优状态，只不过运算规则从加法变成了乘积。但在处理乘法时，负数带来的影响是绝对不能忽视的。在加法运算中，负数顶多是让和变小，但在乘法运算中，两个负数相乘反而会得到正数，这种特殊情况不容忽视。&lt;/p&gt;
&lt;p&gt;在处理乘积最大子数组问题时，由于负数的存在会导致乘积的正负性质发生翻转，因此状态转移需同步维护两个递推状态：以当前位置结尾的最大乘积 $dp_{max}[i]$ 与最小乘积 $dp_{min}[i]$ 。对于当前元素 $nums[i]$ ，最大乘积的更新取决于三个候选值的极值：前一位置最大乘积与当前值的积 $dp_{max}[i-1] \times nums[i]$ 、前一位置最小乘积与当前负值的积 $dp_{min}[i-1] \times nums[i]$ ，以及当前元素自身 $nums[i]$ 。&lt;/p&gt;
&lt;p&gt;其核心的递推逻辑可以直观地表达为：&lt;/p&gt;
&lt;p&gt;$$
dp_{max}[i] = \max(nums[i], , dp_{max}[i-1] \cdot nums[i], dp_{min}[i-1] \cdot nums[i])
$$&lt;/p&gt;
&lt;p&gt;$$
dp_{min}[i] = \min(nums[i], , dp_{max}[i-1] \cdot nums[i], dp_{min}[i-1] \cdot nums[i])
$$&lt;/p&gt;
&lt;p&gt;通过同步维护最大与最小状态，算法在一次 $O(n)$ 的线性扫描中，实现了对正数增长、负数翻转以及零点截断三种情况的全覆盖。这种建模方式简化了复杂的分类讨论，高效地从局部决策推导出了全局最优。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 2e4 + 100;
int a[MAXN];
ll dp_max[MAXN], dp_min[MAXN];

int main() {
    int n; cin &amp;gt;&amp;gt; n;
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; a[i];
    }

    dp_max[0] = dp_min[0] = a[0];
    ll ans = a[0];
    for (int i = 1; i &amp;lt; n; i++) {
        ll cur = a[i];
        ll nxt_max = max({(ll)cur, dp_max[i - 1] * cur, dp_min[i - 1] * cur});
        ll nxt_min = min({(ll)cur, dp_max[i - 1] * cur, dp_min[i - 1] * cur});
        
        dp_max[i] = nxt_max;
        dp_min[i] = nxt_min;

        ans = max(ans, dp_max[i]);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;子序列累加和问题&lt;/h1&gt;
&lt;p&gt;在动态规划的框架下，连续子数组问题的 &lt;strong&gt;逻辑深度&lt;/strong&gt; 源于其对于 &lt;strong&gt;空间关联性&lt;/strong&gt; 的严格限定。由于元素必须在序列中保持物理上的连续，算法决策被局限在特定的范围内，这使得我们能够通过以当前位置为终点的状态定义，将全局的最优性通过逐点推进的递推关系进行精准刻画。这种对位置连续性的依赖，消除了决策过程中的歧义，为动态规划在处理区间问题时提供了稳固的逻辑支撑。&lt;/p&gt;
&lt;p&gt;与之不同，子序列累加和问题彻底解构了这种空间限制，赋予了元素极高的选取自由度。解决没有任何额外限制的子序列问题 &lt;strong&gt;无需设计复杂的动态规划&lt;/strong&gt; ，因为仅凭简单的贪心逻辑选取所有正数即为最优解，这直接导致了该类问题失去了动态规划的讨论价值。因此子序列累加和这类题目，必然建立在特定的约束条件之上。只有引入这些额外的限制，子序列累加和问题才具备了动态规划的讨论价值，从而使状态转移能够有效地刻画复杂的决策过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;打家劫舍问题&lt;/strong&gt; 就是子序列问题的经典范例。该类题目通过增加 &lt;strong&gt;相邻元素不可同时选取&lt;/strong&gt; 的规则，彻底打破了贪心算法的全局最优性。在此类约束下，每一个位置的决策不再独立，而是必须通过状态转移权衡是否选取当前元素的收益。这种对决策状态的精准划分，将原本简单的累加问题转化为了 &lt;strong&gt;带条件的路径选择&lt;/strong&gt; ，迫使算法在递推过程中必须动态评估前序决策所带来的约束限制。&lt;/p&gt;
&lt;h3&gt;打家劫舍对偶问题&lt;/h3&gt;
&lt;p&gt;打家劫舍的对偶问题是指在选择子序列时，约束条件从 “相邻位置不能同时选择” 变为 “相邻位置必选其一” 。这意味着在序列中，任何两个相邻元素都不允许被同时跳过。从对偶的角度来看，这一问题可以直接套用打家劫舍的思路：要求选中的元素满足相邻必选其一，等同于未选中的元素满足相邻不能同时选取。因此，如果目标是最小化选中元素的总和，本质上就是用数组总和减去该序列中满足打家劫舍约束的最大独立集之和。&lt;/p&gt;
&lt;p&gt;如果我们考虑从正面直接求解，则需要利用状态定义来排除连续漏选的可能性。设 $dp[i][0]$ 为不选择当前元素时的最小累加和，$dp[i][1]$ 为选择当前元素时的最小累加和。状态之间的转移方式如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若当前位置 $i$ 不选，则前一个位置 $i-1$ 必须被选中&lt;/p&gt;
&lt;p&gt;$$
dp[i][0] = dp[i-1][1]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若当前位置 $i$ 被选中，则前一个位置 $i-1$ 选或不选均可&lt;/p&gt;
&lt;p&gt;$$
dp[i][1] = \min(dp[i-1][0], , dp[i-1][1]) + nums[i]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种建模方式直接在状态转移逻辑中整合了约束条件，通过前后位置的相互依赖，确保了选择结果的合法性。得益于这种状态设计的严密性，我们能够充分发挥动态规划的递推优势，仅通过一次线性扫描，即可高效解析出满足全部结构约束的最优方案。&lt;/p&gt;
&lt;h2&gt;打家劫舍基础题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，&lt;strong&gt;如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给定一个代表每个房屋存放金额的非负整数数组 &lt;code&gt;nums&lt;/code&gt; ，计算你 &lt;strong&gt;在不惊动警报装置的情况下&lt;/strong&gt; ，一夜之内能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 400$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示房屋的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示每个房屋中存放的金额。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{N-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示你能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 2 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
2 7 9 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;环数组打家劫舍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber-ii/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 &lt;strong&gt;围成一圈&lt;/strong&gt; ，这意味着第一个房屋和最后一个房屋是紧挨着的。同时，相邻的房屋装有相互连通的防盗系统，&lt;strong&gt;如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给定一个代表每个房屋存放金额的非负整数数组 &lt;code&gt;nums&lt;/code&gt; ，计算你 &lt;strong&gt;在不惊动警报装置的情况下&lt;/strong&gt; ，今晚能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示房屋的数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示每个房屋中存放的金额。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{N-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示你能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
2 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 2 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;序列最大中位数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class128/Code05_MaximizeMedian1.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $n$ 的数组 &lt;code&gt;arr&lt;/code&gt; 。请在所有合法的子序列中，找到最大的中位数。&lt;/p&gt;
&lt;p&gt;一个合法的子序列定义为：在原数组中，任意相邻的两个数至少要有一个被挑选所组成的子序列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：中位数的定义为 &lt;strong&gt;上中位数&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$[1, 2, 3, 4]$ 的上中位数是 $2$ 。&lt;/li&gt;
&lt;li&gt;$[1, 2, 3, 4, 5]$ 的上中位数是 $3$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq arr[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 $arr$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$arr_0 \quad arr_1 \quad \ldots \quad arr_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示所有合法子序列中可能的最大中位数。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;多区间累加和问题&lt;/h1&gt;
&lt;p&gt;多区间累加和问题通常根据 &lt;strong&gt;区间长度是否固定&lt;/strong&gt; 分为两类。当长度固定时，区间结构规整，仅由起点决定；而长度可变时，左右端点均可自由浮动，导致 &lt;strong&gt;状态设计与转移逻辑&lt;/strong&gt; 更为复杂。从建模上看，此类问题结合了子数组与子序列的特性：区间内部要求元素绝对连续，继承了子数组的连续性；区间之间则允许互不相邻，体现了子序列的灵活性。这种复合结构要求我们在状态转移时，既要维护好内部的连续性，也要协调好各区间间的跳跃逻辑。&lt;/p&gt;
&lt;p&gt;除了区间长度，&lt;strong&gt;区间个数&lt;/strong&gt; 同样是此类问题的核心约束。对于区间长度固定的情形，如果不限制区间个数，那么我们可以用最简单的线性动态规划解决，只需要预处理以每个位置开头的定长区间累加和即可。而对于长度可变的情形，如果不限制区间个数，那么问题则会直接退化为 &lt;strong&gt;最普通的子序列累加和问题&lt;/strong&gt; ，我们只需贪心选取所有正数即可求解，从而失去了动态规划探讨的深度。接下来，我们将针对这两种情形，详细探讨在引入区间个数限制后，如何完成 &lt;strong&gt;状态定义与转移逻辑&lt;/strong&gt; 的构建。&lt;/p&gt;
&lt;h3&gt;固定长度区间选取&lt;/h3&gt;
&lt;p&gt;在区间长度固定的场景下，假设每个区间的长度均为 $L$ ，且总共需要选取 $K$ 个区间。由于长度固定，任何合法的区间均可由其右端点 $i$ 唯一确定为 $[i-L+1, i]$ ，这使得问题的自由度归结为 &lt;strong&gt;对一系列预设区间起点的选择&lt;/strong&gt; 。为了优化计算，我们可以先利用前缀和预处理出所有长度为 $L$ 的区间和，将原问题简化为在这些预设区间中，寻找 $K$ 个互不重叠且总和最大的组合。&lt;/p&gt;
&lt;p&gt;预处理阶段，通过前缀和数组 $pre$ 快速计算以 $i$ 为结尾的区间和 $w[i]$：&lt;/p&gt;
&lt;p&gt;$$
w[i] = pre[i] - pre[i-L]
$$&lt;/p&gt;
&lt;p&gt;在构建动态规划模型时，我们设 $dp[i][j]$ 表示在前 $i$ 个位置中选取 $j$ 个长度为 $L$ 的区间所能达到的最大累加和。此时，在每一个位置 $i$ 处，决策逻辑简化为对当前结尾区间的选择与否：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max\Big( dp[i-1][j], , dp[i-L][j-1] + w[i] \Big)
$$&lt;/p&gt;
&lt;p&gt;方程的第一项 $dp[i-1][j]$ 代表不选择以 $i$ 为结尾的区间，直接继承前一位置的最优结果；第二项则代表强制选择区间 $[i-L+1, i]$ ，由于区间长度为 $L$ ，为了保证不重叠，上一个区间的结束位置必须在 $i-L$ 之前。这种建模方式利用了区间长度固定的特性，使冲突范围变得极其确定，整体时间复杂度稳定在 $O(nK)$ 。&lt;/p&gt;
&lt;h3&gt;自由长度区间选取&lt;/h3&gt;
&lt;p&gt;相比之下，当区间长度不再固定时，每个区间的左右端点 $[l, r]$ 均可自由选取，这显著提升了问题的灵活性与复杂度。我们设 $dp[i][j]$ 表示在前 $i$ 个位置中选取 $j$ 个互不重叠区间的最大总和。此时，核心决策逻辑在于：要么不以当前位置 $i$ 作为任何区间的结尾，直接继承 $dp[i-1][j]$ ；要么令第 $j$ 个区间在 $i$ 处结束，此时该区间的左端点 $t$ 成为决定性的变量。&lt;/p&gt;
&lt;p&gt;若通过前缀和 $pre$ 进行初步建模，并枚举最后一个区间的起点 $t$ ，其转移方程可以表示为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max\Big( dp[i-1][j], , \max_{0 \leq t &amp;lt; i} \big( dp[t][j-1] + pre[i] - pre[t] \big) \Big)
$$&lt;/p&gt;
&lt;p&gt;直接枚举起点 $t$ 会导致 $O(n^2K)$ 的计算复杂度，在处理大规模数据时效率较低。为了优化性能，我们可以将第二项的结构重新整理为以下形式：&lt;/p&gt;
&lt;p&gt;$$
(dp[t][j-1] - pre[t]) + pre[i]
$$&lt;/p&gt;
&lt;p&gt;观察可以发现，对于固定的区间个数 $j$ ，当 $i$ 向后扫描时，$pre[i]$ 是一个确定的偏移量，而我们需要寻找的是历史跨度中 $dp[t][j-1] - pre[t]$ 的最大值。因此，我们只需在扫描过程中实时维护一个变量 $best$：&lt;/p&gt;
&lt;p&gt;$$
best = \max_{0 \le t &amp;lt; i} \big( dp[t][j-1] - pre[t] \big)
$$&lt;/p&gt;
&lt;p&gt;此时，状态转移方程简化为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \max\big( dp[i-1][j], , pre[i] + best \big)
$$&lt;/p&gt;
&lt;p&gt;在每一步更新完 $dp[i][j]$ 后，随即利用当前的 $dp[i][j-1] - pre[i]$ 去更新 $best$ ，从而为后续位置的计算提供支持。通过这种方式，我们消除了对起点 $t$ 的显式枚举，使单次状态转移的时间复杂度降至 $O(1)$ ，整体复杂度也成功从 $O(n^2K)$ 优化至 $O(nK)$ 。这一优化的核心逻辑在于 &lt;strong&gt;将区间枚举转化为历史最优值的增量维护&lt;/strong&gt; ，从而完成了从搜索型转移到常数级维护的结构升级。&lt;/p&gt;
&lt;h2&gt;神奇的魔法卷轴&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class071/Code03_MagicScrollProbelm.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个数组 &lt;code&gt;nums&lt;/code&gt; ，其中可能包含正数、负数和 $0$ 。&lt;/p&gt;
&lt;p&gt;现在你有 &lt;strong&gt;2 个&lt;/strong&gt; 魔法卷轴。每个魔法卷轴可以将数组 &lt;code&gt;nums&lt;/code&gt; 中 &lt;strong&gt;连续&lt;/strong&gt; 的一段区间内的所有数字全部变成 $0$ 。&lt;/p&gt;
&lt;p&gt;你可以选择不使用卷轴，或者使用 $1$ 个或 $2$ 个卷轴。请返回在经过操作后，数组整体累加和可能的最大值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^6$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 &lt;code&gt;nums&lt;/code&gt; 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在至多使用 $2$ 个魔法卷轴后，数组可能达到的最大累加和。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;无重叠子数组和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，找出三个长度为 &lt;code&gt;k&lt;/code&gt; 、互不重叠的子数组，它们的全部元素和最大。&lt;/p&gt;
&lt;p&gt;我们需要返回这三个子数组起始下标的列表。如果存在多个结果，返回字典序最小的一个。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 2 \times 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] &amp;lt; 2^{16}$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq \lfloor nums.length / 3 \rfloor$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，其中 $n$ 为数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 &lt;code&gt;nums&lt;/code&gt; 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出三个整数，表示这三个互不重叠子数组的起始下标。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8 2
1 2 1 2 6 7 5 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 3 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 2
1 2 1 2 1 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;环形最大子段和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1121&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给出由 $n$ 个整数（可能为负数）组成的环状序列 $a_1, a_2, \ldots, a_n$ 。&lt;/p&gt;
&lt;p&gt;你需要从该环状序列中选出两段 &lt;strong&gt;互不重叠&lt;/strong&gt; 的非空连续子段，使得这两段子段中所有整数的和最大。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$-10^4 \leq a_i \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示环状序列的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示环状序列中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示环状最大两段子段和的最大值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
2 -4 3 -1 2 -4 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;也是对偶问题&lt;/p&gt;
&lt;h2&gt;反转数组最大和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class071/Code05_ReverseArraySubarrayMaxSum.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个数组 &lt;code&gt;nums&lt;/code&gt; 。现在允许你随意选择数组中 &lt;strong&gt;连续&lt;/strong&gt; 的一段区间进行 &lt;strong&gt;翻转&lt;/strong&gt; 操作（即子数组逆序调整）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如：对于数组 &lt;code&gt;[1, 2, 3, 4, 5, 6]&lt;/code&gt; ，翻转下标范围 &lt;code&gt;[2, 4]&lt;/code&gt; 的子数组后，得到 &lt;code&gt;[1, 2, 5, 4, 3, 6]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请返回在 &lt;strong&gt;必须进行一次&lt;/strong&gt; 翻转操作后，所得数组的 &lt;strong&gt;最大子数组累加和&lt;/strong&gt; 是多少。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^6$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组 &lt;code&gt;nums&lt;/code&gt; 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_0 \quad nums_1 \quad \ldots \quad nums_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示必须翻转一次后，数组可能达到的最大子数组累加和。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;划分动态规划问题&lt;/h1&gt;
&lt;p&gt;划分动态规划的核心逻辑在于将一个给定序列 &lt;strong&gt;完整地&lt;/strong&gt; 切分为若干连续的子段。这种题目类型要求序列中的每个元素都必须归属于某一个子段，从而实现对原序列的完全覆盖。在这种结构下，全局最优解的构建依赖于当前位置 $i$ 的最优状态，它是由前序某个切分点 $t$ 的子问题解与最后一段 $[t+1, i]$ 的代价直接拼接而成，从而实现从局部最优到整体最优的递推。&lt;/p&gt;
&lt;p&gt;与多区间累加和问题相比，划分 DP 的核心在于元素分配的 &lt;strong&gt;强制性&lt;/strong&gt; 。在区间选取问题中，我们可以灵活选择每个区间，从而通过放弃部分元素来规避负收益。但在划分 DP 的场景下，每个元素都必须被归入某个具体的段落中。这种全覆盖的约束迫使状态设计必须围绕切分点展开，从而确立了递推过程中的邻接依赖关系。&lt;/p&gt;
&lt;h3&gt;最优型划分问题&lt;/h3&gt;
&lt;p&gt;最优划分型问题通常不限制划分的具体个数，其目标是寻找一种分割方案，使得最终的累积得分或总代价达到全局最优。在这类问题中，状态定义通常采用一维形式，$dp[i]$ 表示前 $i$ 个元素达成最优划分时的得分。由于段数 $k$ 是未知的，在处理位置 $i$ 时，需要枚举所有可能的最后一个切分点 $t$ ，从而从历史的最优状态完成状态转移：&lt;/p&gt;
&lt;p&gt;$$
dp[i] = \text{opt}_{0 \leq t &amp;lt; i} { dp[t] \otimes cost(t+1, i) }
$$&lt;/p&gt;
&lt;p&gt;这种建模方式将整个序列视为一个递归的切分决策序列，通过遍历所有合法的切分点 $t$ ，不断将当前区间 $[t+1, i]$ 的代价与前缀最优状态进行组合。最终，算法通过对所有历史切分可能性的比对，直接在状态空间中锁定全局最优的路径。&lt;/p&gt;
&lt;h3&gt;约束型划分问题&lt;/h3&gt;
&lt;p&gt;划分约束型问题对 &lt;strong&gt;划分次数 K&lt;/strong&gt; 进行了明确限制。对于至多划分为 $K$ 份的需求，在实际处理中可以直接将其转化为恰好划分为 $j$ 份（ $1 \leq j \leq K$ ）的逻辑进行求解。在具体实现中，仅需按照恰好划分的流程计算出所有可能的段数结果，最后在结果收集阶段，从 $dp[n][1]$ 到 $dp[n][K]$ 中遍历并提取全局最优值。&lt;/p&gt;
&lt;p&gt;在恰好划分为 $K$ 份的场景下，状态定义通常采用二维形式，$dp[i][j]$ 表示前 $i$ 个元素被精确划分为 $j$ 段时的最优值。在进行状态转移时，通过枚举最后一个划分点 $t$ 的位置来推进：假设最后一段的范围是 $[t+1, i]$ ，那么前 $t$ 个元素必然已经构成了 $j-1$ 段。其状态转移方程如下：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \text{opt}_{j-1 \leq t &amp;lt; i} { dp[t][j-1] \otimes cost(t+1, i) }
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恰好型划分问题&lt;/strong&gt; 与 &lt;strong&gt;多区间累加和问题&lt;/strong&gt; 在状态转移方程上非常相似，但核心区别在于 &lt;strong&gt;状态继承项&lt;/strong&gt; 的处理。在多区间累加和问题中，由于元素允许被舍弃，方程必定包含 $dp[i-1][j]$ 这一项，代表当前元素不属于任何区间。但在恰好型划分问题中，由于区间必须实现全覆盖，方程必定不含 $dp[i-1][j]$ 这一项。&lt;/p&gt;
&lt;p&gt;当 $cost(t+1, i)$ 具备可拆分性（如区间累加和 $pre[i] - pre[t]$ ）时，该模型同样可以进行结构优化。通过将方程整理为关于 $t$ 的独立项与关于 $i$ 的偏移项，可以引入辅助变量 $best$ 来实时维护历史最优值：&lt;/p&gt;
&lt;p&gt;$$
best = \max_{j-1 \leq t &amp;lt; i} { dp[t][j-1] - pre[t] }
$$&lt;/p&gt;
&lt;p&gt;此时，状态转移方程简化为 $dp[i][j] = pre[i] + best$ 。在扫描过程中，每一步更新完 $dp[i][j]$ 后，随即利用当前的 $dp[i][j-1] - pre[i]$ 去更新 $best$ ，从而为后续位置的计算提供支持。这种优化方式消除了对起点 $t$ 的显式枚举，使单次状态转移的时间复杂度降至 $O(1)$ ，整体复杂度也从 $O(n^2K)$ 降至 $O(nK)$ 。&lt;/p&gt;
&lt;h2&gt;字符串乘积最值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1018&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;今年是 $2000$ 年，若某个同学能够解答出这个问题，他将获得 “中学生数学竞赛” 的冠军。这个问题是：有一个正整数 $N$（由 $n$ 位数字组成），现在你需要在这 $n$ 位数字之间插入 $k$ 个乘号，使得最终的乘积最大。&lt;/p&gt;
&lt;p&gt;需要注意以下两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;乘号不能放在数字的首尾。&lt;/li&gt;
&lt;li&gt;插入的 $k$ 个乘号将 $N$ 分成了 $k + 1$ 个部分，这 $k + 1$ 个部分的乘积即为最终结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$6 \leq n \leq 40$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个长度为 $n$ 的正整数 $N$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示能够得到的最大乘积。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 2
1231
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;62
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;统计单词的个数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1026&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给出一个长度不超过 $200$ 的字符串 $S$ ，该字符串由 $p$ 个部分组成，每个部分长度均为 $20$ 。现在需要将字符串 $S$ 分成 $k$ 个部分，使得这 $k$ 个部分中包含的单词总数最多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单词统计规则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单词在一个部分中出现，指的是该单词是该部分的子串。&lt;/li&gt;
&lt;li&gt;如果一个单词在某个位置已经作为开头被统计过，则在该位置不能再统计其他单词。&lt;/li&gt;
&lt;li&gt;单词表中的单词可能存在包含关系，需按上述规则计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq p \leq 10$&lt;/li&gt;
&lt;li&gt;$2 \leq k \leq 40$&lt;/li&gt;
&lt;li&gt;$1 \leq s \leq 6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $p$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $p$ 行，每行包含一个长度为 $20$ 的字符串，共同拼接成总字符串 $S$ 。&lt;/li&gt;
&lt;li&gt;下一行包含一个整数 $s$ ，表示单词表中的单词个数。&lt;/li&gt;
&lt;li&gt;接下来的 $s$ 行，每行包含一个单词。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$p \quad k$&lt;/p&gt;
&lt;p&gt;$S_{part1}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$S_{partP}$&lt;/p&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$word_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$word_s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多能统计到的单词个数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 3
thisisabookyouareaoh
4
is
a
ok
sab
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;环形的数字游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1043&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在一条圆圈上有 $n$ 个数字（可能为负数），现在要将这 $n$ 个数字划分为 $m$ 个连续的部分（每个部分至少包含一个数字）。每一部分所有数字之和对 $10$ 取模（取模运算的结果应当在 $0$ 到 $9$ 之间，例如 $-1 \bmod 10 = 9$ ）。&lt;/p&gt;
&lt;p&gt;最后将这 $m$ 个部分得到的数值相乘，请问最终的乘积最大和最小分别是多少。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 50$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 9$&lt;/li&gt;
&lt;li&gt;$-10^4 \leq a_i \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含一个整数，按顺序表示圆圈上的数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$a_1$&lt;/p&gt;
&lt;p&gt;$a_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$a_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行输出一个整数表示可能的最小乘积。&lt;/li&gt;
&lt;li&gt;第二行输出一个整数表示可能的最大乘积。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 2
4
3
-1
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
81
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/namelessstory/p/19013752&quot;&gt;【elainafan】动态规划之划分型DP&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】最长递增子序列问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-increasing-subsequence/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-increasing-subsequence/</guid><description>记录一些 ACM 常见题型</description><pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;经典动态规划思想&lt;/h1&gt;
&lt;p&gt;最长递增子序列（LIS）是一个非常典型的 &lt;strong&gt;偏序型动态规划问题&lt;/strong&gt; 。最自然的思路是从朴素的状态设计入手：定义 $dp[i]$ 表示 &lt;strong&gt;以第 i 个元素结尾&lt;/strong&gt; 的最长递增子序列长度。那么在计算 $dp[i]$ 时，我们只需要枚举所有 $j &amp;lt; i$ ，如果 $a[j] &amp;lt; a[i]$ ，就可以尝试用 $dp[j] + 1$ 来更新 $dp[i]$ 。这种做法本质上是在所有可能的前驱中寻找最优决策，因此时间复杂度为 $O(n^2)$ 。&lt;/p&gt;
&lt;p&gt;然而这种朴素做法虽然直观，但效率偏低。进一步观察可以发现：在位置 $i$ 之前的所有历史状态中，存在大量冗余的状态。如果某个位置 $j_1$ 的数值更小且对应的状态值更大，那么对于后续的任何决策而言，位置 $j_1$ 显然都比数值更大且状态值更小的位置 $j_2$ 更具竞争力。&lt;/p&gt;
&lt;h3&gt;单调二分优化&lt;/h3&gt;
&lt;p&gt;基于这一思想，我们不再显式维护每个位置的最优值，而是换一个角度：维护一个数组 $d[len]$ ，表示 &lt;strong&gt;长度为 len 的递增子序列，其最小可能的结尾元素是多少&lt;/strong&gt; 。换句话说，我们对 “同一长度的所有递增子序列” 只保留结尾最小的那一个，因为结尾越小，未来可扩展的空间就越大，这显然是更优的代表状态。&lt;/p&gt;
&lt;p&gt;这个数组具有一个重要性质：它一定是 &lt;strong&gt;单调递增的&lt;/strong&gt; 。原因在于，长度为 $len+1$ 的递增子序列必然是在某个长度为 $len$ 的递增子序列后面添加一个更大的元素得到的，因此其结尾元素一定严格大于对应长度为 $len$ 的结尾元素。并且由于 $d[len]$ 中存储的是 &lt;strong&gt;所有长度为 len 的递增子序列中结尾最小的那个值&lt;/strong&gt; ，它本身已经是该长度下的最优代表状态，所以无论长度为 $len+1$ 的序列是从哪个具体的长度为 $len$ 的序列转移过来的，其结尾元素都必然大于这个最小结尾。因此必然有：&lt;/p&gt;
&lt;p&gt;$$
d[len] &amp;lt; d[len+1]
$$&lt;/p&gt;
&lt;p&gt;这一单调性保证了我们可以对其进行二分查找。于是，当遍历到一个新元素 $a[i]$ 时，我们只需要在 $d$ 数组中找到 &lt;strong&gt;第一个大于等于 $a[i]$&lt;/strong&gt; 的位置，用 $a[i]$ 去更新它；如果不存在这样的元素，则说明可以扩展最长长度。这样，每个元素只需一次二分查找，时间复杂度从 $O(n^2)$ 优化为 $O(n \log n)$ 。&lt;/p&gt;
&lt;h3&gt;树状数组优化&lt;/h3&gt;
&lt;p&gt;回到问题本身，我们也可以从最初的动态规划状态转移出发，对朴素的 $O(n^2)$ 枚举过程进行进一步优化。仍然定义 $dp[i]$ 表示 &lt;strong&gt;以第 i 个元素结尾的最长递增子序列长度&lt;/strong&gt; 。根据递增子序列的性质，如果存在 $j&amp;lt;i$ 且满足 $a[j] &amp;lt; a[i]$ ，那么就可以从位置 $j$ 转移到位置 $i$ ，因此有转移关系：&lt;/p&gt;
&lt;p&gt;$$
dp[i] = \max_{j&amp;lt;i,a[j]&amp;lt;a[i]} dp[j] + 1
$$&lt;/p&gt;
&lt;p&gt;在朴素算法中，需要枚举所有满足条件的 $j$ ，从而导致 $O(n^2)$ 的时间复杂度。进一步观察可以发现，这个转移实际上只关心一件事情：在所有 &lt;strong&gt;数值小于 $a[i]$&lt;/strong&gt; 的元素中，最大的 $dp$ 值是多少。也就是说，我们并不关心这些元素具体出现在序列的哪个位置，只需要知道当前所有满足 $a[j] &amp;lt; a[i]$ 的状态中的最优值即可。&lt;/p&gt;
&lt;p&gt;为了能高效地完成这一查询，我们可以先对序列中的数值进行 &lt;strong&gt;离散化&lt;/strong&gt; 。将所有出现过的数值进行排序并重新编号，使得每个元素 $a[i]$ 对应一个排名 $rank[i]$ 。这样一来，所有满足 $a[j] &amp;lt; a[i]$ 的元素，就等价于排名位于区间 $[1,rank[i]-1]$ 的元素。在此基础上，可以利用 &lt;strong&gt;树状数组&lt;/strong&gt; 来维护这些信息。&lt;/p&gt;
&lt;p&gt;树状数组的每个节点存储当前某个数值范围内的最大 $dp$ 值，并支持两种基本操作：一是查询区间 $[1,x]$ 的最大值，二是更新某个位置的最大值。当遍历到元素 $a[i]$ 时，首先在树状数组中查询区间 $[1,rank[i]-1]$ 的最大值，得到当前所有小于 $a[i]$ 的元素所对应的最大 $dp$ 值，记为 $best$ ，于是：&lt;/p&gt;
&lt;p&gt;$$
dp[i] = best + 1
$$&lt;/p&gt;
&lt;p&gt;随后再用 $dp[i]$ 去更新树状数组中位置 $rank[i]$ 的值即可。&lt;/p&gt;
&lt;p&gt;由于树状数组的单次查询和更新操作复杂度均为 $O(\log n)$ ，整个算法的时间复杂度可以降低为 $O(n\log n)$ 。与前面的单调二分优化不同，树状数组优化的核心思想并不是通过维护最优代表状态来压缩状态空间，而是 &lt;strong&gt;直接从动态规划转移关系出发，将原本需要枚举的转移过程转化为数据结构上的区间查询问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;最长上升子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1020&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;某国为了防御敌国的导弹袭击，开发了一种导弹拦截系统。但这种系统存在一个缺陷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一发炮弹可以到达任意高度；&lt;/li&gt;
&lt;li&gt;之后每一发炮弹的高度 &lt;strong&gt;都不能高于前一发炮弹的高度&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于系统仍在试用阶段，目前只有 &lt;strong&gt;一套拦截系统&lt;/strong&gt; ，因此可能无法拦截所有导弹。&lt;/p&gt;
&lt;p&gt;现在给出导弹依次飞来的高度序列，要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一套拦截系统最多能够拦截多少枚导弹&lt;/strong&gt; ；&lt;/li&gt;
&lt;li&gt;如果希望拦截所有导弹，&lt;strong&gt;最少需要多少套拦截系统&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq h_i \leq 5 \times 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一套系统最多能拦截的导弹数量&lt;/li&gt;
&lt;li&gt;拦截所有导弹所需的最少系统数量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;389 207 155 300 299 170 158 65
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;首先考虑题目的第一个问题：&lt;strong&gt;一套拦截系统最多能够拦截多少枚导弹&lt;/strong&gt; 。根据题目规则，拦截系统发射的炮弹高度必须满足后一发不高于前一发，也就是说拦截的导弹高度序列必须是一个 &lt;strong&gt;不上升序列&lt;/strong&gt; 。因此问题可以直接转化为：在给定的导弹高度序列中，求一个 &lt;strong&gt;最长不上升子序列&lt;/strong&gt; 。这一问题与经典的最长递增子序列问题是完全对称的，可以通过动态规划求解，利用二分优化将复杂度降低到 $O(n \log n)$ 。&lt;/p&gt;
&lt;p&gt;接下来考虑第二个问题：&lt;strong&gt;如果要拦截所有导弹，最少需要多少套拦截系统&lt;/strong&gt; 。每一套拦截系统所拦截的导弹序列都必须满足不上升的性质，因此本质上是在问：如何将整个序列划分成尽量少的若干个 &lt;strong&gt;不上升子序列&lt;/strong&gt; 。从反向的角度来看，如果存在一个严格上升的子序列，那么其中的每一个元素都无法被同一套系统拦截，因为后一枚导弹高度高于前一枚导弹，违背了拦截规则。因此，这个上升序列中的每一个元素都必须由 &lt;strong&gt;不同的拦截系统&lt;/strong&gt; 来处理。&lt;/p&gt;
&lt;p&gt;由此可以得到一个重要结论：需要的最少拦截系统数量，恰好等于该序列的 &lt;strong&gt;最长严格上升子序列&lt;/strong&gt; 的长度。换句话说，最长上升子序列中的每一个元素都至少需要一套独立的系统才能完成拦截。这个结论也可以从序列划分的角度理解：将一个序列划分为若干个不上升序列，其最少划分数量等于该序列的最长上升子序列长度。&lt;/p&gt;
&lt;p&gt;综上这道题可以拆分为两个经典问题：第一个是求 &lt;strong&gt;最长不上升子序列&lt;/strong&gt; ，第二个是求 &lt;strong&gt;最长严格上升子序列&lt;/strong&gt; 。二者都可以利用最长递增子序列的标准算法进行求解，整体时间复杂度可以做到 $O(n \log n)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组K递增问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-operations-to-make-the-array-k-increasing&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个下标从 $0$ 开始包含 $n$ 个正整数的数组 &lt;code&gt;arr&lt;/code&gt; ，和一个正整数 &lt;code&gt;k&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果对于每个满足 $k \leq i \leq n - 1$ 的下标 $i$ 都有 $arr[i-k] \leq arr[i]$ ，那么称数组 &lt;code&gt;arr&lt;/code&gt; 是 &lt;strong&gt;k 递增&lt;/strong&gt; 的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比方说，&lt;code&gt;arr = [4, 1, 5, 2, 6, 2]&lt;/code&gt; 对于 $k = 2$ 是 k 递增的，因为：
&lt;ul&gt;
&lt;li&gt;$arr[0] \leq arr[2] (4 \leq 5)$&lt;/li&gt;
&lt;li&gt;$arr[1] \leq arr[3] (1 \leq 2)$&lt;/li&gt;
&lt;li&gt;$arr[2] \leq arr[4] (5 \leq 6)$&lt;/li&gt;
&lt;li&gt;$arr[3] \leq arr[5] (2 \leq 2)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;相反，&lt;code&gt;arr = [4, 1, 5, 2, 6, 2]&lt;/code&gt; 对于 $k = 3$ 不是 k 递增的，因为 $arr[0] &amp;gt; arr[3]$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一步操作中，你可以选择一个下标 $i$ 并将 $arr[i]$ 改成 &lt;strong&gt;任意&lt;/strong&gt; 正整数。&lt;/p&gt;
&lt;p&gt;请你返回使数组 &lt;code&gt;arr&lt;/code&gt; 变成 &lt;strong&gt;k 递增&lt;/strong&gt; 的 &lt;strong&gt;最少&lt;/strong&gt; 操作次数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq arr.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq arr[i], k \leq arr.length$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $arr$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$arr_0 \quad arr_1 \quad \ldots \quad arr_{N-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示使数组变成 k 递增的最少操作次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 1
5 4 3 2 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 2
4 1 5 2 6 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 3
4 1 5 2 6 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;取模分组问题&lt;/p&gt;
&lt;h2&gt;嵌套的信封问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/russian-doll-envelopes&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个二维整数数组 &lt;code&gt;envelopes&lt;/code&gt; ，其中 $envelopes[i] = [w_i, h_i]$ ，表示第 $i$ 个信封的宽度和高度。&lt;/p&gt;
&lt;p&gt;当另一个信封的宽度和高度都比这个信封大的时候，这个信封就可以放进另一个信封里，如同俄罗斯套娃一样。&lt;/p&gt;
&lt;p&gt;请计算 &lt;strong&gt;最多&lt;/strong&gt; 能有多少个信封能形成一组 “俄罗斯套娃” 信封（即可以把一个信封放到另一个信封里面）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：不允许旋转信封。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq envelopes.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$envelopes[i].length == 2$&lt;/li&gt;
&lt;li&gt;$1 \leq w_i, h_i \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示信封的数量。&lt;/li&gt;
&lt;li&gt;接下来 $N$ 行，每行包含两个整数 $w_i$ 和 $h_i$ ，表示每个信封的宽度和高度。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$w_1 \quad h_1$&lt;/p&gt;
&lt;p&gt;$w_2 \quad h_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$w_N \quad h_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最多能套娃的信封数目。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
5 4
6 4
6 7
2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 1
1 1
1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;二维偏序问题&lt;/p&gt;
&lt;h2&gt;最长数对链问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-length-of-pair-chain&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个包含 $n$ 个数对的数组 &lt;code&gt;pairs&lt;/code&gt; ，其中 $pairs[i] = [left_i, right_i]$ 且 $left_i &amp;lt; right_i$ 。&lt;/p&gt;
&lt;p&gt;现在，我们定义一种 &lt;strong&gt;跟随&lt;/strong&gt; 关系，当且仅当 $b &amp;lt; c$ 时，数对 $p2 = [c, d]$ 才可以跟在 $p1 = [a, b]$ 后面。我们用这种形式来构造 &lt;strong&gt;数对链&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;请你返回能够形成的序列链的 &lt;strong&gt;最长长度&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;你不需要用到所有的数对，你可以以任何顺序选择其中的一些数对来构造。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == pairs.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 1000$&lt;/li&gt;
&lt;li&gt;$-1000 \leq left_i &amp;lt; right_i \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数对的数量。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行包含两个整数 $left_i$ 和 $right_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$left_1 \quad right_1$&lt;/p&gt;
&lt;p&gt;$left_2 \quad right_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$left_n \quad right_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最长数对链的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 2
2 3
3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 2
7 8
4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;带修递增子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8776&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的整数序列：$A_1, A_2, \ldots, A_N$ 。你可以从中选择一个 &lt;strong&gt;连续&lt;/strong&gt; 的区间，该区间的长度为 $K$ ，并将该区间内的所有数字全部修改成任意一个相同的整数。&lt;/p&gt;
&lt;p&gt;请你返回在进行至多一次上述修改操作后，整个序列的 &lt;strong&gt;最长不下降子序列&lt;/strong&gt; 的长度最大是多少。&lt;/p&gt;
&lt;p&gt;最长不下降子序列的定义是：从原序列中按顺序取出若干个数字，使得这些数字满足非递减关系。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq K \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i \leq 10^6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $K$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组 $A$ 中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad K$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在修改后能获得的最长不下降子序列的最大长度。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 1
1 4 2 8 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;使数组严格递增&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/make-array-strictly-increasing/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你两个整数数组 &lt;code&gt;arr1&lt;/code&gt; 和 &lt;code&gt;arr2&lt;/code&gt; ，返回使 &lt;code&gt;arr1&lt;/code&gt; 严格递增所需要的最小操作次数。&lt;/p&gt;
&lt;p&gt;每一步操作中，你可以分别从 &lt;code&gt;arr1&lt;/code&gt; 和 &lt;code&gt;arr2&lt;/code&gt; 中各选出一个索引，分别为 $i$ 和 $j$ ，用 &lt;code&gt;arr2[j]&lt;/code&gt; 替换 &lt;code&gt;arr1[i]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果无法让 &lt;code&gt;arr1&lt;/code&gt; 严格递增，请返回 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq arr1.length, arr2.length \leq 2000$&lt;/li&gt;
&lt;li&gt;$0 \leq arr1[i], arr2[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，分别表示 $arr1$ 和 $arr2$ 的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示 $arr1$ 中的元素。&lt;/li&gt;
&lt;li&gt;第三行包含 $M$ 个整数，表示 $arr2$ 中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$arr1_0 \quad arr1_1 \quad \ldots \quad arr1_{N-1}$&lt;/p&gt;
&lt;p&gt;$arr2_0 \quad arr2_1 \quad \ldots \quad arr2_{M-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示使 $arr1$ 严格递增所需的最小操作次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
1 5 3 6
1 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
1 5 3 6
4 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
1 5 3 6
1 6 3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法题单】最长公共子序列问题</title><link>https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-common-subsequence/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/dp-problems/longest-common-subsequence/</guid><description>记录一些 ACM 常见题型</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;经典动态规划思想&lt;/h1&gt;
&lt;p&gt;最长公共子序列（LCS）与最长公共子串（LCSubstr）是动态规划中极具代表性的两个经典问题。它们本身并不复杂，但其建模方式却贯穿了大量序列类题目。许多看似形式各异的挑战，本质上都可以追溯到这两类问题的思想变形，只是在状态定义或转移条件上进行了不同程度的包装。&lt;/p&gt;
&lt;p&gt;从建模逻辑来看，&lt;strong&gt;子序列类问题的本质是寻找非连续的元素子集&lt;/strong&gt; 。在定义状态时，$dp[i][j]$ 往往表示考虑到第一个序列前 $i$ 个元素与第二个序列前 $j$ 个元素时的全局最优解，而不需要强求当前元素必须被选中。这种定义天然允许跳过某些元素，其转移过程通常围绕选或不选的决策展开。两序列当前字符相等时，意味着我们找到了交集的一个新成员，状态由 $dp[i-1][j-1]$ 累加递增；两序列当前字符不相等时，则通过继承 $dp[i-1][j]$ 或 $dp[i][j-1]$ 的已有结果来保持交集的最大化。&lt;/p&gt;
&lt;p&gt;与之相对，&lt;strong&gt;子串类问题则要求目标序列必须在物理空间上绝对连续&lt;/strong&gt; 。这使得状态定义必须显式刻画以某个位置结尾的信息，例如定义 $dp[i][j]$ 为以两个序列当前字符结尾的最长公共后缀长度。在这种约束下，一旦序列中断了连续性，即当前元素无法与前序构成紧密关联，其状态价值必须被强制重置。因此这类问题的转移更加局部，高度依赖当前位置之间的直接匹配关系，而无法像子序列样跨越节点。&lt;/p&gt;
&lt;h3&gt;最短公共父序列&lt;/h3&gt;
&lt;p&gt;最短公共超序列（SCS）同样是动态规划的一个经典问题，其核心目标是构造一个最短的序列，使得两个原始序列都能作为其子序列完整出现。如果将 LCS 理解为提取序列间的 &lt;strong&gt;最大交集&lt;/strong&gt; ，那么 SCS 则是寻找它们的 &lt;strong&gt;最小并集&lt;/strong&gt; 。为了实现这一目标，我们通常有两种截然不同但逻辑互补的建模视角。&lt;/p&gt;
&lt;p&gt;第一种视角是将 SCS 看作 LCS 的 &lt;strong&gt;对偶问题&lt;/strong&gt; 。为了使构造长度降至最低，问题的本质在于 &lt;strong&gt;最大化复用&lt;/strong&gt; 两个序列中的相同字符。这种复用逻辑深刻体现了 &lt;strong&gt;容斥原理&lt;/strong&gt;：并集的规模等于两个集合规模之和减去交集规模。在这种逻辑下，我们无需重新设计算法，可以直接通过下式计算超序列的长度：&lt;/p&gt;
&lt;p&gt;$$
Length(SCS) = Length(S_1) + Length(S_2) - Length(LCS)
$$&lt;/p&gt;
&lt;p&gt;在具体构造字符串时，这种做法相当于以 &lt;strong&gt;LCS 为骨架&lt;/strong&gt; 。我们先确定公共字符的位置，然后在这些骨架字符的空隙中，按原有的相对顺序依次填入两个序列各自特有的差异字符。&lt;/p&gt;
&lt;p&gt;另一种视角则是直接针对 &lt;strong&gt;必选约束下的最优覆盖&lt;/strong&gt; 进行建模。我们设 $dp[i][j]$ 为使 $S_1$ 前 $i$ 个字符和 $S_2$ 前 $j$ 个字符成为其子序列的最短超序列长度。当字符相等时（ $S_1[i] == S_2[j]$ ），我们获得了一个珍贵的复用机会，该字符在结果中只占一个位宽，状态由下式转移：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i-1][j-1] + 1
$$&lt;/p&gt;
&lt;p&gt;当字符不等时（ $S_1[i] \neq S_2[j]$ ），我们不再像处理 LCS 那样简单地抛弃不匹配的部分，而是必须在结果中 &lt;strong&gt;接纳当前字符&lt;/strong&gt; 以保证覆盖的完整性。此时，我们需要在 “接纳 $S_1$ 的当前字符” 与 “接纳 $S_2$ 的当前字符” 这两个分支中，选择 &lt;strong&gt;代价更小&lt;/strong&gt; 的路径继续推进：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = \min(dp[i-1][j], , dp[i][j-1]) + 1
$$&lt;/p&gt;
&lt;p&gt;这种直接建模方式不仅解决了长度计算问题，更为我们提供了一套精准的 &lt;strong&gt;回溯流程&lt;/strong&gt; 。通过观察 $dp$ 表的决策路径，我们能够按图索骥地构造出这个包含所有原始信息的最小字符序列。&lt;/p&gt;
&lt;h2&gt;最长公共子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-common-subsequence/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定两个字符串 &lt;code&gt;text1&lt;/code&gt; 和 &lt;code&gt;text2&lt;/code&gt; ，返回这两个字符串的最长 &lt;strong&gt;公共子序列&lt;/strong&gt; 的长度。如果不存在 &lt;strong&gt;公共子序列&lt;/strong&gt; ，返回 $0$ 。一个字符串的 &lt;strong&gt;子序列&lt;/strong&gt; 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如，&lt;code&gt;&quot;ace&quot;&lt;/code&gt; 是 &lt;code&gt;&quot;abcde&quot;&lt;/code&gt; 的子序列，但 &lt;code&gt;&quot;aec&quot;&lt;/code&gt; 不是 &lt;code&gt;&quot;abcde&quot;&lt;/code&gt; 的子序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个字符串的 &lt;strong&gt;公共子序列&lt;/strong&gt; 是这两个字符串所共同拥有的子序列。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq text1.length, text2.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text1&lt;/code&gt; 和 &lt;code&gt;text2&lt;/code&gt; 仅由小写英文字符组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $text1$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $text2$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$text1$&lt;/p&gt;
&lt;p&gt;$text2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示这两个字符串的最长公共子序列的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abcde
ace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abc
abc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abc
def
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;可以从前往后匹配也可以从后往前匹配&lt;/p&gt;
&lt;h2&gt;不同子序列个数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/distinct-subsequences/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你两个字符串 &lt;code&gt;s&lt;/code&gt; 和 &lt;code&gt;t&lt;/code&gt; ，统计并返回在 &lt;code&gt;s&lt;/code&gt; 的 &lt;strong&gt;子序列&lt;/strong&gt; 中 &lt;code&gt;t&lt;/code&gt; 出现的个数，结果需要对 $10^9 + 7$ 取模。&lt;/p&gt;
&lt;p&gt;题目测试用例保证压力在可控范围内，结果不会超过 $2^{63} - 1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length, t.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 和 &lt;code&gt;t&lt;/code&gt; 由英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $s$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $t$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$t$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在 $s$ 的子序列中 $t$ 出现的个数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;rabbbit
rabbit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;babgbag
bag
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最短公共父序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/shortest-common-supersequence/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给出两个字符串 &lt;code&gt;str1&lt;/code&gt; 和 &lt;code&gt;str2&lt;/code&gt; ，返回同时以 &lt;code&gt;str1&lt;/code&gt; 和 &lt;code&gt;str2&lt;/code&gt; 作为 &lt;strong&gt;子序列&lt;/strong&gt; 的最短字符串。如果答案不止一个，则可以返回满足条件的 &lt;strong&gt;任意一个&lt;/strong&gt; 答案。&lt;/p&gt;
&lt;p&gt;如果从字符串 $T$ 中删除一些字符（也可能不删除），可以形成字符串 $S$ ，那么 $S$ 就是 $T$ 的子序列。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq str1.length, str2.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;str1&lt;/code&gt; 和 &lt;code&gt;str2&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $str1$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $str2$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$str1$&lt;/p&gt;
&lt;p&gt;$str2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个字符串，表示满足条件的最短公共超序列。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abac
cab
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cabac
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaaaaaaa
aaaaaaaa
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaaaaaaa
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最低的编辑距离&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/edit-distance/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你两个单词 &lt;code&gt;word1&lt;/code&gt; 和 &lt;code&gt;word2&lt;/code&gt; ， 请返回将 &lt;code&gt;word1&lt;/code&gt; 转换成 &lt;code&gt;word2&lt;/code&gt; 所使用的最少操作数。&lt;/p&gt;
&lt;p&gt;你可以对一个单词进行如下三种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入一个字符&lt;/li&gt;
&lt;li&gt;删除一个字符&lt;/li&gt;
&lt;li&gt;替换一个字符&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$0 \leq word1.length, word2.length \leq 500$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;word1&lt;/code&gt; 和 &lt;code&gt;word2&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $word1$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $word2$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$word1$&lt;/p&gt;
&lt;p&gt;$word2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示将 $word1$ 转换成 $word2$ 所需的最少操作数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;horse
ros
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;intention
execution
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;交错字符串问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/interleaving-string/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定三个字符串 &lt;code&gt;s1&lt;/code&gt; 、&lt;code&gt;s2&lt;/code&gt; 、&lt;code&gt;s3&lt;/code&gt;，请你帮忙验证 &lt;code&gt;s3&lt;/code&gt; 是否是由 &lt;code&gt;s1&lt;/code&gt; 和 &lt;code&gt;s2&lt;/code&gt; &lt;strong&gt;交错&lt;/strong&gt; 组成的。&lt;/p&gt;
&lt;p&gt;两个字符串 &lt;code&gt;s&lt;/code&gt; 和 &lt;code&gt;t&lt;/code&gt; 交错的定义与过程如下，其中每个字符串都会被分割成若干 &lt;strong&gt;非空&lt;/strong&gt; 子字符串：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$s = s_1 + s_2 + \ldots + s_n$&lt;/li&gt;
&lt;li&gt;$t = t_1 + t_2 + \ldots + t_m$&lt;/li&gt;
&lt;li&gt;如果 $|n - m| \leq 1$ ，且满足下述任意一种拼接顺序，则认为其是交错的：
&lt;ul&gt;
&lt;li&gt;$s_1 + t_1 + s_2 + t_2 + s_3 + t_3 + \ldots$&lt;/li&gt;
&lt;li&gt;$t_1 + s_1 + t_2 + s_2 + t_3 + s_3 + \ldots$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：$a + b$ 表示字符串 $a$ 和 $b$ 连接。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$0 \leq s1.length, s2.length \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq s3.length \leq 200$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s1&lt;/code&gt; 、&lt;code&gt;s2&lt;/code&gt; 、&lt;code&gt;s3&lt;/code&gt; 均由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $s1$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $s2$ 。&lt;/li&gt;
&lt;li&gt;第三行包含一个字符串，表示 $s3$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s1$&lt;/p&gt;
&lt;p&gt;$s2$&lt;/p&gt;
&lt;p&gt;$s3$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;s3&lt;/code&gt; 是由 &lt;code&gt;s1&lt;/code&gt; 和 &lt;code&gt;s2&lt;/code&gt; 交错组成的，输出 &lt;code&gt;true&lt;/code&gt; ；否则，输出 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aabcc
dbbca
aadbbcbcac
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aabcc
dbbca
aadbbbaccc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;
&quot;&quot;
&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;正则表达式匹配&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/regular-expression-matching&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;s&lt;/code&gt; 和一个字符规律 &lt;code&gt;p&lt;/code&gt; ，请你来实现一个支持 &lt;code&gt;&apos;.&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;*&apos;&lt;/code&gt; 的正则表达式匹配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;.&apos;&lt;/code&gt; 匹配任意单个字符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;*&apos;&lt;/code&gt; 匹配零个或多个前面的那一个元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所谓匹配，是要涵盖 &lt;strong&gt;整个&lt;/strong&gt; 字符串 &lt;code&gt;s&lt;/code&gt; 的，而不是部分字符串。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 20$&lt;/li&gt;
&lt;li&gt;$1 \leq p.length \leq 20$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 只包含从 &lt;code&gt;a-z&lt;/code&gt; 的小写字母&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; 只包含从 &lt;code&gt;a-z&lt;/code&gt; 的小写字母，以及字符 &lt;code&gt;.&lt;/code&gt; 和 &lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;保证每次出现字符 &lt;code&gt;*&lt;/code&gt; 时，前面都匹配到有效的字符&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $s$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $p$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$p$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果输入字符串 $s$ 与字符规律 $p$ 匹配，输出 &lt;code&gt;true&lt;/code&gt; ；否则，输出 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aa
a
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aa
a*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ab
.*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;最长公共子序列+完全背包思想&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.luogu.com/article/ml584xxs&quot;&gt;【Luogu 博客】最长公共子序列问题&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法题单】拓扑排序算法相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/graph-problems/topological-sort/topological-sort/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/graph-problems/topological-sort/topological-sort/</guid><description>记录一些 ACM 常见题型</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;拓扑排序算法原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;拓扑排序&lt;/strong&gt; 是针对有向无环图的一种线性排序方法，旨在构造出一个不违背任何局部依赖关系的全局顺序。其核心逻辑在于处理顶点间的先后约束：若图中存在有向边 $u \rightarrow v$ ，则在最终序列中顶点 $u$ 必须排在 $v$ 之前。从数学角度看，这实际上是偏序关系的一种 &lt;strong&gt;线性扩展&lt;/strong&gt; ，即将元素间原本零散、部分可比的先后关系转化为一个完整的全序序列。由于满足约束的路径往往不止一条，当图中存在互不依赖的顶点时，其相对位置可以灵活调整，因此拓扑排序的结果通常并不唯一。&lt;/p&gt;
&lt;p&gt;作为一种严谨的算法工具，拓扑排序存在的前提是图中 &lt;strong&gt;严格不含有向环&lt;/strong&gt; 。环路的存在意味着顶点间形成了循环依赖，导致先后关系在逻辑上自相矛盾，从而无法构造出任何有效的线性序列。基于这一特性，拓扑排序在算法设计中不仅被用于确定节点处理的先后次序，也常被作为检测有向图是否存在环路的关键手段。通过尝试构建拓扑序，算法可以判定图结构的合法性，从而保证后续逻辑推导的正确性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-type%5Cgraph-problems%5Ctopological-sort%5C%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F.png&quot; alt=&quot;拓扑排序图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;拓扑建模分析&lt;/h2&gt;
&lt;p&gt;拓扑建模的核心在于对 &lt;strong&gt;偏序关系&lt;/strong&gt; 的精确提取。在建模过程中，我们无需刻画元素间所有可能的相对位置，只需聚焦于那些不可违背的先后约束，通过将逻辑依赖转化为有向图中的边定向，使原问题转化为一个有向无环图。建图时应遵循 &lt;strong&gt;最小化约束原则&lt;/strong&gt; ，即仅对必须在前的关系建边。以区间覆盖问题为例，若操作 $1$ 的区间先后被操作 $2$ 和操作 $3$ 覆盖，但最终结果仅受操作 $3$ 决定，那么在建模时只需建立 $1 \rightarrow 3$ 的边，而无需引入 $1 \rightarrow 2$ 等冗余依赖。这种建模方式能有效过滤无效的约束，利用 DAG 的拓扑性质将复杂的网状依赖梳理为有序的线性序列。&lt;/p&gt;
&lt;p&gt;拓扑排序仅适用于有向无环图，但当有环图存在 &lt;strong&gt;步数限制&lt;/strong&gt; 等单调维度时，可以通过分层思想将其转化为 DAG。由于每经过一条边步数必然递增，这种单调性有效打破了原图的环路，使其常与分层图思想挂钩：通过将节点 $u$ 拆分为 $(u, k)$（表示第 $k$ 步到达点 $u$ ），并将原图边 $u \rightarrow v$ 映射为跨层的单向边 $(u, k) \rightarrow (v, k+1)$ ，原本复杂的有环图被拉平为多层 DAG。在这种建模下，层间仅存在单向依赖，使得我们能够结合拓扑排序或线性动态规划，高效解决带约束的路径优化问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;偏序排列拓扑问题&lt;/h1&gt;
&lt;p&gt;拓扑排序的本质是在一个由 &lt;strong&gt;偏序关系&lt;/strong&gt; 所约束的集合中，构造出一个满足所有约束条件的线性排列。换言之，其核心在于将偏序关系扩展为全序关系，从所有合法的线性扩展中选取一个可行解。这种处理方式关注的并非元素之间的数值绝对值，而是它们在逻辑、操作或因果维度上的先后约束。只要问题对象之间存在明确的先后限制，且满足自反性、反对称性与传递性等偏序性质，就可以将问题自然地抽象成 &lt;strong&gt;有向无环图&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在建模过程中，节点代表对象个体，有向边则刻画对象之间 &lt;strong&gt;必须遵守的先后准则&lt;/strong&gt; 。通过这种图论转化，原问题是否存在合法解，便等价于判定图中是否存在环路；若无环，则说明该偏序系统是自洽的，可以构造出合法的线性序列。从这一角度看，拓扑排序是一种通用的偏序问题求解框架。例如，在数论的整除关系中，若 $a \mid b$ ，可视为 $a$ 领先于 $b$ 。这种视角有助于我们迅速识别出题目中的偏序结构，从而转化为标准图论模型。&lt;/p&gt;
&lt;h2&gt;奶牛的排名确认&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2419&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $n$ 头奶牛参加了比赛。比赛共有 $m$ 次单对单的比较。由于某些原因，我们无法得知所有比赛的结果，但我们可以通过已知的比较结果推导出某些隐含的排名关系（如果 A 胜过 B，B 胜过 C，则 A 必然胜过 C）。&lt;/p&gt;
&lt;p&gt;如果对于某一头奶牛，我们能够明确知道它在所有 $n$ 头奶牛中的确切排名（即能够确定它与其余所有 $n-1$ 头奶牛的胜负关系），那么这头奶牛的排名就是确定的。请统计有多少头奶牛的排名是确定的。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 4500$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i, B_i \leq n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ ，分别表示奶牛总数和比较次数。&lt;/li&gt;
&lt;li&gt;接下来的 $m$ 行，每行包含两个整数 $A$ 和 $B$ ，表示奶牛 $A$ 击败了奶牛 $B$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$A_1 \quad B_1$&lt;/p&gt;
&lt;p&gt;$A_2 \quad B_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_m \quad B_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示排名可以确定的奶牛数量。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 5
4 3
4 2
3 2
1 2
2 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;拓扑图随机游走&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P6154&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个包含 $n$ 个点和 $m$ 条有向边的 &lt;strong&gt;有向无环图&lt;/strong&gt; 。你可以从图中的任意一个点出发，并进行随机游走：在当前点，如果该点有出度，则等概率选择一条出边走向下一个点；如果没有出边，则游走停止。&lt;/p&gt;
&lt;p&gt;定义一条路径的权重为其经过的边数（路径长度）。请求出所有可能路径的长度的期望值。具体来说，如果记 $S$ 为所有路径的集合，$L(p)$ 为路径 $p$ 的长度，你需要计算：&lt;/p&gt;
&lt;p&gt;$$
\frac{\sum_{p \in S} L(p)}{|S|}
$$&lt;/p&gt;
&lt;p&gt;其中 $|S|$ 是路径的总数，即从所有可能的起点出发，最终到达任意终点的不同路径的总条数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq u_i, v_i \leq n$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $m$ 行，每行包含两个整数 $u$ 和 $v$ ，表示存在一条从 $u$ 到 $v$ 的有向边。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_m \quad v_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示期望值在模 $998244353$ 下的结果。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
1 2
3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;199648871
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;绿豆蛙最后归宿&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4316&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个包含 $n$ 个点和 $m$ 条有向边的 &lt;strong&gt;有向无环图&lt;/strong&gt; 。绿豆蛙从点 $1$ 出发，目的是到达点 $n$ 。&lt;/p&gt;
&lt;p&gt;在每一个点，绿豆蛙会等概率选择从该点出发的一条出边走向下一个点。求绿豆蛙从点 $1$ 到达点 $n$ 所经过路径长度的期望值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq u_i, v_i \leq n$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 2 \cdot 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq w_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $m$ 行，每行包含三个整数 $u$ 、$v$ 和 $w$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$u_1 \quad v_1 \quad w_1$&lt;/p&gt;
&lt;p&gt;$u_2 \quad v_2 \quad w_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$u_m \quad v_m \quad w_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行一个实数代表答案，四舍五入保留两位小数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 4 
1 2 1 
1 3 2 
2 3 3 
3 4 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7.00
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;幼儿园分配糖果&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3275&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;幼儿园里有 $n$ 个小朋友，老师需要给他们分糖果。小朋友们按照 $1$ 到 $n$ 排成一列，每个小朋友都想要糖果。老师对这些小朋友有一些要求，这些要求共有 $k$ 种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 $X=1$：小朋友 $A$ 和小朋友 $B$ 分到的糖果一样多。&lt;/li&gt;
&lt;li&gt;如果 $X=2$：小朋友 $A$ 分到的糖果少于小朋友 $B$（ $A$ 的糖果数 $+ 1 \leq B$ 的糖果数）。&lt;/li&gt;
&lt;li&gt;如果 $X=3$：小朋友 $A$ 分到的糖果不少于小朋友 $B$（ $B$ 的糖果数 $\leq A$ 的糖果数）。&lt;/li&gt;
&lt;li&gt;如果 $X=4$：小朋友 $A$ 分到的糖果多于小朋友 $B$（ $B$ 的糖果数 $+ 1 \leq A$ 的糖果数）。&lt;/li&gt;
&lt;li&gt;如果 $X=5$：小朋友 $A$ 分到的糖果不多于小朋友 $B$（ $A$ 的糖果数 $\leq B$ 的糖果数）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每个小朋友至少要分到 $1$ 颗糖果。在满足所有要求的前提下，求老师至少需要准备多少颗糖果。如果无法满足要求，输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n, k \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq X \leq 5$&lt;/li&gt;
&lt;li&gt;$1 \leq A, B \leq n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $k$ 行，每行包含三个整数 $X$ 、$A$ 和 $B$ ，表示要求的类型及对应的两个小朋友。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$X_1 \quad A_1 \quad B_1$&lt;/p&gt;
&lt;p&gt;$X_2 \quad A_2 \quad B_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$X_k \quad A_k \quad B_k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最少需要的糖果总数，若无法满足，输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;求解戳印的序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/stamping-the-sequence/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;你想要用小写字母组成一个目标字符串 &lt;code&gt;target&lt;/code&gt; 。 开始的时候，序列由 &lt;code&gt;target.length&lt;/code&gt; 个 &lt;code&gt;&apos;?&apos;&lt;/code&gt; 记号组成。而你有一个小写字母印章 &lt;code&gt;stamp&lt;/code&gt; 。在每个回合，你可以将印章放在序列上，并将序列中的每个字母替换为印章上的相应字母。你最多可以进行 &lt;code&gt;10 * target.length&lt;/code&gt;  个回合。&lt;/p&gt;
&lt;p&gt;举个例子，如果初始序列为 &quot;?????&quot; ，而你的印章 &lt;code&gt;stamp&lt;/code&gt; 是 &lt;code&gt;&quot;abc&quot;&lt;/code&gt; ，那么在第一回合，你可以得到 &quot;abc??&quot; 、&quot;?abc?&quot; 、&quot;??abc&quot; 。（请注意，印章必须完全包含在序列的边界内才能盖下去）&lt;/p&gt;
&lt;p&gt;如果可以印出序列，那么返回一个数组，该数组由每个回合中被印下的最左边字母的索引组成。如果不能印出序列，就返回一个空数组。例如，如果序列是 &quot;ababc&quot; ，印章是 &lt;code&gt;&quot;abc&quot;&lt;/code&gt; ，那么我们就可以返回与操作 &quot;?????&quot; -&amp;gt; &quot;abc??&quot; -&amp;gt; &quot;ababc&quot; 相对应的答案 &lt;code&gt;[0, 2]&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq stamp.length \leq target.length \leq 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stamp&lt;/code&gt; 和 &lt;code&gt;target&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串，表示 $stamp$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串，表示 $target$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$stamp$&lt;/p&gt;
&lt;p&gt;$target$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出若干个整数表示印章需要戳印的位置及顺序。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abc
ababc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;abca
aabcaca
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;本题的核心挑战在于处理 &lt;strong&gt;盖章过程中的覆盖与重叠&lt;/strong&gt; 。由于后盖的印章会覆盖先盖的字符，正向推导印章顺序非常困难，因此我们采用一种 &lt;strong&gt;逆向还原的思维&lt;/strong&gt;：尝试将 &lt;code&gt;target&lt;/code&gt; 字符串逐步还原为初始的问号状态。每一次逆向盖章相当于找到一个当前区间，其非通配符部分与 &lt;code&gt;stamp&lt;/code&gt; 完全匹配。我们将这些匹配位置视作已解决的局部并转化为通配符，表示它们在正向操作中可以被更晚的操作覆盖，从而消除了当前位置对这些字符的依赖。&lt;/p&gt;
&lt;p&gt;为了系统化处理这种覆盖依赖，我们将每一个可能的戳印起始位置视为图中的一个 &lt;strong&gt;节点&lt;/strong&gt; 。对于每个位置 $i$ ，其覆盖范围为 $[i, i+m-1]$ ，若该区间内某个字符与印章对应位置不匹配，则说明该字符必须由其他戳印来完成最终定型。在此建模下，每个戳印位置的 &lt;strong&gt;入度&lt;/strong&gt; 可以定义为该区间内与印章不匹配的字符数量。入度为 $0$ 的节点意味着该位置的所有字符要么已经匹配，要么已被其他操作变为通配符，因此它可以作为当前还原步骤的合法候选。&lt;/p&gt;
&lt;p&gt;在执行过程中，我们首先初始化所有戳印位置的入度并建立字符索引与位置的映射。初始入度为 $0$ 的位置首先入队，每从队列取出一个位置执行逆向戳印，便检查该区间内尚未处理的字符，并令所有覆盖这些字符的戳印位置入度减 $1$ 。一旦某个位置入度归零，说明其约束已消除，随即具备执行条件。最终得到的序列是正向盖章顺序的 &lt;strong&gt;倒序排列&lt;/strong&gt; ，若执行完所有操作后 &lt;code&gt;target&lt;/code&gt; 已完全转化，则翻转序列输出，否则说明目标序列无法构造。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;塔扬缩点拓扑问题&lt;/h1&gt;
&lt;p&gt;许多算法题呈现出明显的 &lt;strong&gt;拓扑性质&lt;/strong&gt; ，逻辑上显然需要利用拓扑排序来梳理执行顺序或依赖关系，但题目给出的图结构并未明确为有向无环图。为了打破这种僵局，通常需要先采用 &lt;strong&gt;Tarjan 算法&lt;/strong&gt; 或 Kosaraju 算法识别图中的所有 &lt;strong&gt;强连通分量&lt;/strong&gt; ，将每个分量收缩为单个 &lt;strong&gt;超节点&lt;/strong&gt; 并保留跨分量的原始边。通过这一转换，原图中的环路被封装进节点内部，超节点之间则构成了严格的 &lt;strong&gt;有向无环图&lt;/strong&gt; ，将原本杂乱的循环约束转化为了清晰的层次结构。&lt;/p&gt;
&lt;p&gt;一旦图结构通过缩点被简化为有向无环图，便可以顺理成章地引入拓扑排序及动态规划等后续算法。在缩点后的有向无环图上，每个超节点可以承载原强连通分量内部的 &lt;strong&gt;权重总和&lt;/strong&gt; 或状态集合，从而允许我们在宏观路径上进行 &lt;strong&gt;递推计算&lt;/strong&gt; 。这种强连通分量缩点与拓扑排序的结合已成为处理复杂有向图问题的 &lt;strong&gt;标准范式&lt;/strong&gt;：它通过先行消除不可区分的环结构，再在宏观层面梳理剩余的 &lt;strong&gt;偏序关系&lt;/strong&gt; ，使得原本杂乱无章的约束变得结构透明且易于计算。&lt;/p&gt;
&lt;h2&gt;在风筝之上奔跑&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4742&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;一天的活动过后，所有学生都停下来欣赏夜空下点亮的风筝。$Curtis , Nishikino$ 想要以更近的视角感受一下，所以她跑到空中的风筝上去了！每只风筝上的灯光都有一个亮度 $k_i$ 。由于风的作用，一些风筝缠在了一起。但这并不会破坏美妙的气氛，缠在一起的风筝会将灯光汇聚起来，形成更亮的光源！&lt;/p&gt;
&lt;p&gt;$Curtis , Nishikino$ 已经知道了一些风筝间的关系，比如给出一对风筝 $(a, b)$ ，这意味着她可以从 $a$ 跑到 $b$ 上去，但是不能返回。&lt;/p&gt;
&lt;p&gt;现在，请帮她找到一条路径（她可以到达一只风筝多次，但只在第一次到达时她会感受上面的灯光），使得她可以感受到最多的光亮。同时请告诉她这条路径上单只风筝的最大亮度，如果有多条符合条件的路径，输出能产生最大单只风筝亮度的答案。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$0 &amp;lt; n \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$0 &amp;lt; m \leq 5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$0 &amp;lt; k \leq 200$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数 $k_i$ 。&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行，每行包含两个整数 $a$ 和 $b$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$k_1 \quad k_2 \quad \ldots \quad k_n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad b_1$&lt;/p&gt;
&lt;p&gt;$a_2 \quad b_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$a_m \quad b_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行，包含两个整数。$Curtis$ 在计算出的路径上感受到的亮度和，这条路径上的单只风筝最大亮度。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 5
8 9 11 6 7
1 2
2 3
3 4
4 5
5 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;41 11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/graph/topo/&quot;&gt;【OI WiKi】拓扑排序相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/miaopan/p/14632167.html&quot;&gt;【淼畔】Tarjan 算法与缩点技巧&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com/article/exhzn8ks&quot;&gt;【Luogu 博客】Tarjan 缩点&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/training/42933&quot;&gt;【Luogu 题单】拓扑排序专题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】枚举技巧与枚举优化</title><link>https://xingguang641.com/posts/acm/acm-note/enumeration/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/enumeration/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;基础枚举技巧论&lt;/h1&gt;
&lt;p&gt;在算法实现中，&lt;strong&gt;枚举是极为常见且基础的一类操作&lt;/strong&gt; 。无论是遍历状态空间、构造候选方案，还是对中间结果进行验证，枚举都承担着将抽象问题转化为具体计算过程的作用。由于其形式直接、逻辑清晰，枚举往往是解题过程中最早被采用的手段之一，也常常作为更复杂算法的出发点或验证手段。&lt;strong&gt;在许多问题中，即便最终解法并非暴力枚举，枚举仍然在思路推导与结构分析阶段发挥着重要作用&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;然而，枚举的实现方式并非只有简单的多重循环这一种选择。通过合理的 &lt;strong&gt;状态表示、遍历顺序以及构造方式&lt;/strong&gt; ，可以使枚举过程更贴合问题的结构特征，从而在表达上更加简洁、逻辑上更加清晰。这类技巧主要关注的是 &lt;strong&gt;枚举过程的组织方式本身&lt;/strong&gt; ，虽然不一定改变问题的规模或复杂度，但往往能够降低实现难度，并为后续的推导与改写提供更稳定的基础。&lt;/p&gt;
&lt;h2&gt;子集的枚举技巧&lt;/h2&gt;
&lt;p&gt;在算法竞赛中，&lt;strong&gt;子集枚举&lt;/strong&gt; 是解决诸多问题的基础。对于一个含有 $n$ 个元素的集合，其子集的总数为 $2^n$ 。当 $n$ 的规模较小时，通过遍历所有可能的组合来寻找最优解或统计合法方案是一种极具通用性的策略。高效且正确地写出子集枚举逻辑，是深入学习状态压缩动态规划等进阶算法的先决条件。&lt;/p&gt;
&lt;p&gt;最直观的子集枚举方式，是按照元素顺序，对每一个元素依次做 “选或不选” 的决策。这种思路与人类思考子集的方式完全一致，其自然实现形式就是深度优先搜索。设集合大小为 $n$ ，我们从第 $0$ 个元素开始处理，在递归的第 $i$ 层决定元素 $a_i$ 是否被加入当前子集。当所有元素都被处理完时，就得到了一个完整子集。整个搜索树是一棵深度为 $n$ 的二叉树，总共包含 $2^n$ 个叶子节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a;    // 原集合
vector&amp;lt;int&amp;gt; cur;  // 当前子集
int n;

void dfs(int i) {
    if (i == n) {
        // 此时 cur 就是一个子集
        // 在这里处理 cur
        return;
    }

    // 不选 a[i]
    dfs(i + 1);

    // 选 a[i]
    cur.push_back(a[i]);
    dfs(i + 1);
    cur.pop_back();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种 DFS 枚举方式的特点在于结构非常清晰，递归层数与元素个数一一对应，并且在枚举过程中可以自然地加入剪枝条件或提前返回的逻辑。因此，当题目需要在枚举过程中动态维护信息、或根据部分选择判断可行性时，这种写法往往最容易控制和修改。&lt;/p&gt;
&lt;p&gt;另一种同样基础的非迭代方法是 &lt;strong&gt;使用二进制掩码枚举子集&lt;/strong&gt; 。其核心思想是用一个整数的二进制表示来刻画子集状态：第 $i$ 位为 &lt;code&gt;1&lt;/code&gt; 表示选择第 $i$ 个元素，为 &lt;code&gt;0&lt;/code&gt; 表示不选择。这样，从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;(1&amp;lt;&amp;lt;n)-1&lt;/code&gt; 的所有整数，恰好一一对应集合的所有子集。对每一个状态，只需检查每一位是否为 &lt;code&gt;1&lt;/code&gt; 即可还原当前子集包含哪些元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a;
int n;

for (int mask = 0; mask &amp;lt; (1 &amp;lt;&amp;lt; n); mask++) {
    vector&amp;lt;int&amp;gt; subset;
    for (int i = 0; i &amp;lt; n; i++) {
        if (mask &amp;amp; (1 &amp;lt;&amp;lt; i)) {
            subset.push_back(a[i]);
        }
    }
    // subset 即为当前子集
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种枚举方式的优势在于状态表示极为紧凑，判断元素是否被选只需一次位运算，并且非常适合与状态压缩动态规划等算法结合使用。从本质上看，它与 DFS 枚举并没有任何区别，只是将 “选或不选” 的递归决策过程，直接编码进了二进制表示中。&lt;/p&gt;
&lt;p&gt;在位掩码表示的基础上，存在一种在 &lt;strong&gt;状态压缩动态规划&lt;/strong&gt; 中极为关键且高频使用的技巧：&lt;strong&gt;在给定一个特定集合 $S$ 的前提下，枚举它的所有子集&lt;/strong&gt; 。在处理如子集 DP 或需要遍历 “某个状态的所有子状态” 的场景时，我们并不需要从 $0$ 盲目枚举到 $2^n-1$ ，而是仅需聚焦于那些满足 “属于 $S$ 的子集” 的状态。为了追求极致的执行效率，通常采用如下经典循环结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = S; j; j = (j - 1) &amp;amp; S) {
    // j 是 S 的一个非空子集，在这里执行逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段简洁的代码能够 &lt;strong&gt;不重不漏&lt;/strong&gt; 地按照递减顺序枚举出 $S$ 的所有非空子集。其核心原理在于利用了二进制减法的特性：当我们对当前子集 $j$ 执行 &lt;code&gt;j - 1&lt;/code&gt; 操作时，会将 $j$ 最右侧的 &lt;code&gt;1&lt;/code&gt; 变为 &lt;code&gt;0&lt;/code&gt; ，并将其右侧所有的位翻转为 &lt;code&gt;1&lt;/code&gt; ；随后通过与原集合 $S$ 进行 &lt;strong&gt;按位与&lt;/strong&gt; 运算，系统会自动抹除掉不属于 $S$ 的多余位。这一过程精准地锁定了在数值上 &lt;strong&gt;严格小于 $j$ 且仍属于 $S$ 子集&lt;/strong&gt; 的最大整数，从而实现了对子集空间的高效遍历。&lt;/p&gt;
&lt;p&gt;这种写法在解决划分型动态规划等高级算法问题时具有无可替代的地位。相比于先枚举所有掩码再进行逻辑判断的朴素做法，该技巧通过直接操作二进制位，将总的时间复杂度优化至 $O(3^n)$ ，远优于 $O(4^n)$ 的常规实现。该算法从位运算的底层逻辑出发，通过消除无效状态的冗余迭代，使代码逻辑与集合论的数学结构达成了深度契合。&lt;/p&gt;
&lt;h2&gt;排列的枚举技巧&lt;/h2&gt;
&lt;p&gt;在排列问题中，枚举的核心不再是选择，而是 &lt;strong&gt;在不重复使用元素的前提下，确定每一个位置放什么&lt;/strong&gt; 。因此，最自然的建模方式是：把排列看成一个长度为 $n$ 的序列，枚举第 $0$ 位、第 $1$ 位直到第 $n-1$ 位依次放入的元素。这种视角下，排列枚举几乎可以直接套用 DFS 的框架。&lt;/p&gt;
&lt;p&gt;最基础的做法是 &lt;strong&gt;DFS 按位置填数&lt;/strong&gt; 。递归深度表示当前已经确定了多少个位置，在第 &lt;code&gt;pos&lt;/code&gt; 层，从所有尚未使用过的元素中任选一个放到当前位置即可。为了保证每个元素只使用一次，通常会维护一个 &lt;code&gt;used&lt;/code&gt; 数组，用来标记哪些元素已经被选过。递归到深度为 $n$ 时，当前序列就是一个完整排列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a;      // 原始元素
vector&amp;lt;int&amp;gt; perm;   // 当前构造中的排列
vector&amp;lt;bool&amp;gt; used;
int n;

void dfs(int pos) {
    if (pos == n) {
        // perm 是一个完整排列
        return;
    }
    for (int i = 0; i &amp;lt; n; i++) {
        if (used[i]) continue;
        used[i] = true;
        perm.push_back(a[i]);
        dfs(pos + 1);
        perm.pop_back();
        used[i] = false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种基于位置填充的递归模式，在逻辑上直接映射了排列定义的构造过程。这种写法的核心优势在于其具备极高的扩展性：由于每一层递归都明确对应序列中的一个具体位置，可以在枚举过程中直接注入约束条件或维护动态信息，例如在当前层执行剪枝以跳过不符合要求的搜索分支。代价是需要显式维护使用状态，代码略显冗长。&lt;/p&gt;
&lt;p&gt;如果换一个角度看待排列，其实也可以认为排列就是对数组进行一系列交换的结果。基于这种理解，可以得到 &lt;strong&gt;原地交换枚举排列&lt;/strong&gt; 的写法。具体思路是在递归的第 &lt;code&gt;pos&lt;/code&gt; 层，决定哪个元素最终放在位置 &lt;code&gt;pos&lt;/code&gt; ，于是从区间 &lt;code&gt;[pos, n)&lt;/code&gt; 中任选一个元素，与 &lt;code&gt;a[pos]&lt;/code&gt; 交换，然后递归处理下一位。回溯时再交换回来，保证数组状态不被破坏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; a;
int n;

void dfs(int pos) {
    if (pos == n) {
        // 当前 a 即为一个排列
        return;
    }
    for (int i = pos; i &amp;lt; n; i++) {
        swap(a[pos], a[i]);
        dfs(pos + 1);
        swap(a[pos], a[i]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种交换式的构造逻辑，实质上是将待处理的数组划分为已确定和待选择两个动态区间。在每一层递归中，算法通过将后端的一个候选元素交换至当前索引位，隐式地完成了元素的选择与去重，从而避免了维护额外布尔数组的空间开销。从逻辑上看，该方法与显式使用 &lt;code&gt;used&lt;/code&gt; 数组的 DFS 具有对称性：前者通过全局标记锁定可用元素，而此处则通过原地交换改变搜索空间，将所有状态演化直接体现在数组本身的排列组合中。&lt;/p&gt;
&lt;p&gt;除了基于 DFS 的构造型枚举，还有一种常用的方法是 &lt;strong&gt;直接按字典序枚举排列&lt;/strong&gt; 。这种方法并不关心排列是如何构造出来的，而是把排列本身当作一个状态，通过确定的规则在状态之间跳转。只要从字典序最小的排列开始，不断生成下一个排列，就可以在不重复、不遗漏的情况下遍历全部结果。在 C++ 中，这一过程由 &lt;code&gt;next_permutation&lt;/code&gt; 封装完成。只要先对数组排序，然后反复调用该函数即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort(a.begin(), a.end());
do {
    // 当前 a 是一个排列
} while (next_permutation(a.begin(), a.end()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解 &lt;code&gt;next_permutation&lt;/code&gt; 的核心在于：该算法并非任意构造一个新的排列，而是 &lt;strong&gt;在全体排列的字典序序列中，严格生成当前排列的直接后继&lt;/strong&gt; 。为实现这一目标，算法对排列结构进行了精确分析，并通过一组确定性的操作，在保证字典序连续性的同时完成状态转移。其具体过程可以分解为以下三个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从右向左确定首个上升点&lt;/strong&gt;：从序列末尾向左扫描，找到第一个满足 $a[i] &amp;lt; a[i+1]$ 的位置 $i$ 。该位置的存在表明：区间 &lt;code&gt;[i+1, n)&lt;/code&gt; 已经处于在固定前缀 &lt;code&gt;a[0..i]&lt;/code&gt; 条件下的最大排列状态，若要获得字典序更大的排列，唯一可能的修改位置只能位于 $i$ 及其左侧。若不存在这样的 $i$ ，则说明整个序列为非递增序列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在后缀中选择最小的可替换元素交换&lt;/strong&gt;：在区间 &lt;code&gt;[i+1, n)&lt;/code&gt; 中，从右向左查找第一个满足 $a[j] &amp;gt; a[i]$ 的位置 $j$ ，并交换 &lt;code&gt;a[i]&lt;/code&gt; 与 &lt;code&gt;a[j]&lt;/code&gt; 。这一操作的作用在于：在第 $i$ 位对排列进行 &lt;strong&gt;最小幅度的增大&lt;/strong&gt; ，从而保证新排列严格大于原排列，同时又尽可能接近原排列的字典序位置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;将后缀重排为字典序最小形式&lt;/strong&gt;：交换完成后，区间 &lt;code&gt;[i+1, n)&lt;/code&gt; 仍保持非递增状态。为了使整体排列在所有可能的后继中达到字典序最小，只需将该区间反转，使其变为递增序列。此操作确保后缀部分在当前前缀固定的条件下取到最小可能值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正是由于上述三步分别在 &lt;strong&gt;修改位置的选择、增量大小的控制以及后缀排列的最小化&lt;/strong&gt; 三个层面进行了严格约束，&lt;code&gt;next_permutation&lt;/code&gt; 才能够在不依赖递归或额外状态记录的前提下，按照字典序序完整无重复地遍历所有排列。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;贡献法优化枚举&lt;/h1&gt;
&lt;p&gt;在算法设计中，许多问题表面上需要对海量的子集或区间进行穷举，但直接枚举往往会产生极高的计算开销。尤其是在涉及 &lt;strong&gt;组合计数&lt;/strong&gt; 或 &lt;strong&gt;序列特征&lt;/strong&gt; 的复杂问题中，由于不同对象之间存在大量的重叠部分，简单的遍历会导致算法的时间复杂度迅速失控。如何在确保结果准确的前提下，通过转变观察维度来消除原本重复的计算逻辑，是我们优化算法效率的核心课题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;贡献法&lt;/strong&gt; 正是应对此类挑战的一种核心思想。其底层逻辑在于 &lt;strong&gt;逆转对答案构成的观察视角&lt;/strong&gt;：我们不再从宏观层面去逐一处理每一个复杂的整体组合，而是转而统计 &lt;strong&gt;单个元素&lt;/strong&gt; 、&lt;strong&gt;特定操作&lt;/strong&gt; 或 &lt;strong&gt;局部结构&lt;/strong&gt; 在所有合法情形中对最终结果所产生的影响。通过这种 &lt;strong&gt;化整为零&lt;/strong&gt; 的策略，我们将全局的加和问题转化为对各局部元素出现次数的计数问题。&lt;/p&gt;
&lt;h2&gt;局部变化贡献法&lt;/h2&gt;
&lt;p&gt;局部变化贡献法是一种专门处理 &lt;strong&gt;修改为单点操作、查询为全局统计&lt;/strong&gt; 的算法技巧。由于单点修改对整体答案的影响范围非常有限，且目标始终是获取全局统计量，因此我们无需在每次变动后重算全局，而是通过维护一个实时的全局答案来解决。其核心逻辑在于将修改前后的状态差异视为增量修正，通过精准捕捉局部范围内的变化，实现对全局结果的同步更新。&lt;/p&gt;
&lt;p&gt;这种方法的核心在于构建一种 &lt;strong&gt;差分修正&lt;/strong&gt; 的思维范式。它主张放弃对整个序列或集合的重复扫描，转而聚焦于单次修改所引发的 &lt;strong&gt;净变化&lt;/strong&gt; 。在具体实现中，我们通过维护一个全局变量来实时记录答案，根据单点由旧元素变为新元素而产生的贡献差异，直接在全局变量上进行补偿。该策略有效地规避了对恒定状态的冗余计算，使原本耗时的频繁全局统计操作变得极其轻量。&lt;/p&gt;
&lt;h2&gt;最小值之和查询&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc420/tasks/abc420_c&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定两个长度为 $N$ 的整数序列&lt;/p&gt;
&lt;p&gt;$$
A = (A_1, A_2, \ldots, A_N) \quad B = (B_1, B_2, \ldots, B_N)
$$&lt;/p&gt;
&lt;p&gt;以及一个整数 $Q$ 表示查询数量。每个查询会描述如下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输入一个字符 $c_i$ 和两个整数 $X_i, V_i$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 $c_i =$ &lt;code&gt;&quot;A&quot;&lt;/code&gt; ，则将 $A_{X_i}$ 修改为 $V_i$&lt;/li&gt;
&lt;li&gt;若 $c_i =$ &lt;code&gt;&quot;B&quot;&lt;/code&gt; ，则将 $B_{X_i}$ 修改为 $V_i$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在执行完该修改之后，计算并输出：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^N \min(A_k, B_k)
$$&lt;/p&gt;
&lt;p&gt;即序列 $A$ 与序列 $B$ 的对应元素最小值之和。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N, Q \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i, B_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;$c_i$ 只能是字符 &lt;code&gt;&quot;A&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;B&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;$1 \leq X_i \leq N$&lt;/li&gt;
&lt;li&gt;$1 \leq V_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $Q$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示序列 $A$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $N$ 个整数，表示序列 $B$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $Q$ 行中，每行包含一个字符与两个整数，表示一次查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad Q$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots A_N$&lt;/p&gt;
&lt;p&gt;$B_1 \quad B_2 \quad \ldots B_N$&lt;/p&gt;
&lt;p&gt;$c_1 \quad X_1 \quad V_1$&lt;/p&gt;
&lt;p&gt;$c_2 \quad X_2 \quad V_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$c_Q \quad X_Q \quad V_Q$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $Q$ 行，每行对应一个查询的结果，即执行修改并计算最小值之和后的输出。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
3 1 4 1
2 7 1 8
A 2 3
B 3 3
A 1 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
9
9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 3
1
1000000000
A 1 1
A 1 1
A 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
1
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 3
100 100 100 100 100
100 100 100 100 100
A 4 21
A 2 99
B 4 57
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;421
420
420
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;对象交换贡献法&lt;/h2&gt;
&lt;p&gt;对象交换贡献法适用于答案由大量子结构累加而成的情境。其核心逻辑在于 &lt;strong&gt;逆向重构问题维度&lt;/strong&gt;：与其机械地枚举所有可能的整体组合，不如转而分析每一个独立元素、边或子项在最终结果中的 &lt;strong&gt;贡献频次&lt;/strong&gt; 。通过这种从全局搜索向局部统计的视角切换，我们能够将复杂的枚举过程转化为对单个对象映射关系的量化累加，从而在逻辑底层消除冗余判定，大幅压缩计算开销。&lt;/p&gt;
&lt;p&gt;这种 &lt;strong&gt;按位统计贡献&lt;/strong&gt; 的思想在多类算法场景中具有普适性，其精髓在于 &lt;strong&gt;由元素属性倒推全局答案&lt;/strong&gt; 。通过明确单个对象对最终指标的影响量，并将其量化后进行线性加权，该方法能使复杂的逻辑结构变得层次分明。这种分析范式不仅显著提升了执行效率，更是处理高阶组合计数与序列特征问题时，实现计算复杂度量级跨越的核心手段。&lt;/p&gt;
&lt;h2&gt;滑动窗口最大值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc407/tasks/abc407_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的非负整数序列 $A = (A_1, A_2, \ldots, A_N)$ 。对于每一个 $k = 1, 2, \ldots, N$ ，我们考虑所有长度为 $k$ 的 &lt;strong&gt;连续子数组&lt;/strong&gt;（滑动窗口）。对于每一个这样的子数组，求它的最大值，然后将这些最大值求和。&lt;/p&gt;
&lt;p&gt;具体来说，长度为 $k$ 的连续子数组共有 $N-k+1$ 个，它们分别是：&lt;/p&gt;
&lt;p&gt;$$
(A_1, A_2, \ldots, A_k), (A_2, A_3, \ldots, A_{k+1}), \ldots, (A_{N-k+1}, \ldots, A_N)
$$&lt;/p&gt;
&lt;p&gt;我们需要计算这些窗口最大值的总和，并按顺序输出 $k = 1$ 到 $k = N$ 的结果。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq A_i \leq 10^7$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示序列长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示序列的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $N$ 行，第 $i$ 行输出当 $k=i$ 时所有长度为 $k$ 连续子数组最大值之和。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
5 3 4 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;14
13
9
5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8
2 0 2 5 0 5 2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;20
28
27
25
20
15
10
5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;11
9203973 9141294 9444773 9292472 5507634 9599162 497764 430010 4152216 3574307 430010
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;61273615
68960818
69588453
65590626
61592799
57594972
47995810
38396648
28797486
19198324
9599162
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;序列数对的权重&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/contest/1527/problem/C&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数序列 $a = (a_1, a_2, \ldots, a_n)$ 定义此序列的 &lt;strong&gt;权重（weight）&lt;/strong&gt; 为所有满足 $i &amp;lt; j$ 且 $a_i = a_j$ 的 &lt;strong&gt;无序下标对&lt;/strong&gt; 的数量。例如序列 $[1,1,2,2,1]$ 的权重为 $4$ ，因为满足条件的下标对为 $(1,2), (1,5), (2,5), (3,4)$ 。&lt;/p&gt;
&lt;p&gt;给你一个长度为 $n$ 的整数序列 $a$ ，对于该序列的所有 &lt;strong&gt;子段&lt;/strong&gt;（即连续区间）$b = (a_l, a_{l+1}, \ldots, a_r)$ ，计算这些子段的权重之和，并输出最终结果。&lt;/p&gt;
&lt;p&gt;注意，子段定义为从原序列左右两端删除若干（可以为 $0$ 或全部）元素得到的连续区间。输入包含多个测试用例，所有测试用例中所有 $n$ 的总和不超过 $10^5$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq t \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示序列长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示序列元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个测试用例输出一个整数表示所有连续子段权重之和。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
4
1 2 1 1
4
1 2 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;预处理优化枚举&lt;/h1&gt;
&lt;p&gt;在算法设计中，尤其是面对涉及高频查询的场景，若每次请求都从头执行完整的枚举或扫描，往往会导致巨大的计算开销。核心的优化思路在于：&lt;strong&gt;通过预处理机制将计算压力前移&lt;/strong&gt; ，提前处理与具体查询参数无关的公共部分。其本质是将重复性劳动转化为一次性投入，通过利用基础数据中静态不变的属性，构建出高效的辅助逻辑，从而在查询阶段直接调取预存的中间状态，实现对冗余遍历过程的规避。&lt;/p&gt;
&lt;p&gt;在具体实现中，预处理将复杂的逻辑推导简化为 &lt;strong&gt;快速状态合并或直接查表&lt;/strong&gt; 的操作模式。通过将原本随查询波动的动态计算，转化为对预处理结果的常数级调用，系统能够显著压缩查询路径并消除计算冗余。这种策略不仅在静态场景下表现卓越，在配合动态维护结构时同样能维持极高的响应效率，是打破大规模数据处理性能瓶颈、实现极速状态转移的关键手段。&lt;/p&gt;
&lt;h2&gt;选数异或和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8773&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $n$ 的整数序列 $a_1,a_2,\ldots,a_n$ ，以及一个整数 $x$ 。&lt;/p&gt;
&lt;p&gt;现在有 $q$ 次询问，每次询问给定一个区间 $[l,r]$ 。你需要判断是否存在两个不同的下标 $i,j$ ，满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$l \leq i &amp;lt; j \leq r$&lt;/li&gt;
&lt;li&gt;$a_i \oplus a_j = x$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果存在这样的两个数，则输出 &lt;code&gt;yes&lt;/code&gt; ，否则输出 &lt;code&gt;no&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n,q \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq a_i &amp;lt; 2^{20}$&lt;/li&gt;
&lt;li&gt;$0 \leq x &amp;lt; 2^{20}$&lt;/li&gt;
&lt;li&gt;$1 \leq l \leq r \leq n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $n$ 、$q$ 和 $x$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数 $a_i$ 。&lt;/li&gt;
&lt;li&gt;接下来 $q$ 行，每行包含两个整数 $l$ 和 $r$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad q \quad x$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$l_1 \quad r_1$&lt;/p&gt;
&lt;p&gt;$l_2 \quad r_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$l_q \quad r_q$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个询问输出 &lt;code&gt;yes&lt;/code&gt; 或 &lt;code&gt;no&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 4 1
1 2 3 4
1 4
1 2
2 3
3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;yes
no
yes
no
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;针对区间范围内查询某个点对或子区间是否存在，也就是 &lt;strong&gt;区间查区间&lt;/strong&gt; 的问题，一种行之有效的策略是为每个可能的右端点 $r$ 寻找其对应的 &lt;strong&gt;最优左端点 l&lt;/strong&gt; 。我们据此构建出一个 &lt;strong&gt;左端点数组 L&lt;/strong&gt; ，其中 $L[i]$ 记录了以 $i$ 为右端点时，合法结构所能达到的最大左边界下标。由此，判断一个给定区间 $[l, r]$ 是否包含合法结构的准则便可以转化为：&lt;/p&gt;
&lt;p&gt;$$
\max(L_l, \ldots, L_r) \geq l
$$&lt;/p&gt;
&lt;p&gt;这一公式说明了只要区间内存在至少一个右端点对应的 &lt;strong&gt;最佳左端点并未越过询问区间的左边界&lt;/strong&gt; ，则该区间判定为有效。进一步观察可以发现，由于在逻辑构造上每个位置 $i$ 的最优左端点 $L[i]$ 必然满足 $L[i] \leq i$ ，这意味着对于任何 $k &amp;lt; l$ 的位置，其 $L[k]$ 必然也小于 $l$ ，对最终判定的贡献为负。这一关键性质允许我们将区间最值查询进一步简化，即上述公式等价于：&lt;/p&gt;
&lt;p&gt;$$
\max(L_l, \ldots, L_r) = \max(L_0, L_1, \ldots, L_r) \geq l
$$&lt;/p&gt;
&lt;p&gt;通过这一步转化，原本需要使用线段树或 RMQ 维护的区间最值问题，被成功转化成了简单的 &lt;strong&gt;前缀最大值&lt;/strong&gt; 问题。我们仅需在线性时间内预处理出 &lt;strong&gt;前缀最大值数组&lt;/strong&gt; ，即可在 $O(1)$ 的时间内完成任何关于区间合法性的询问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAX = 2e5 + 7;
const int VAL = 1 &amp;lt;&amp;lt; 20;
int n, q, x;
int a[MAX], L[MAX];
int preMax[MAX];
int lastPos[VAL];

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q &amp;gt;&amp;gt; x;
    for (int i = 1; i &amp;lt;= n; i++) {
        cin &amp;gt;&amp;gt; a[i];
    }

    memset(lastPos, 0, sizeof(lastPos));
    for (int i = 1; i &amp;lt;= n; i++) {
        int target = a[i] ^ x;
        L[i] = lastPos[target]; 
        
        lastPos[a[i]] = i;
    }

    preMax[0] = 0;
    for (int i = 1; i &amp;lt;= n; i++) {
        preMax[i] = max(preMax[i - 1], L[i]);
    }

    for (int i = 0; i &amp;lt; q; i++) {
        int l, r;
        cin &amp;gt;&amp;gt; l &amp;gt;&amp;gt; r;
        if (preMax[r] &amp;gt;= l) {
            cout &amp;lt;&amp;lt; &quot;yes&quot; &amp;lt;&amp;lt; endl;
        } else {
            cout &amp;lt;&amp;lt; &quot;no&quot; &amp;lt;&amp;lt; endl;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高桥的心情期望&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc417/tasks/abc417_d&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;高桥君将会收到 $N$ 个礼物。高桥君有一个名为 “情绪” 的参数，这是一个非负整数，每当他收到一个礼物时，情绪值就会发生变动。每个礼物都有三个参数：价值 $P$ 、情绪增加量 $A$ 、情绪减少量 $B$ 。&lt;/p&gt;
&lt;p&gt;根据这些参数，他的情绪变动规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果收到的礼物价值 $P$ &lt;strong&gt;大于或等于&lt;/strong&gt; 他当前的情绪值，他会对礼物感到高兴，情绪值增加 $A$ 。&lt;/li&gt;
&lt;li&gt;如果收到的礼物价值 $P$ &lt;strong&gt;小于&lt;/strong&gt; 他当前的情绪值，他会对礼物感到失望，情绪值减少 $B$ 。但是，如果减完后的情绪值小于 0，则情绪值变为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在给定 $Q$ 个询问，请回答所有询问。在第 $i$ 个询问中，给定一个初始情绪值 $X_i$ ，请计算：如果高桥君的初始情绪值为 $X_i$ ，在按顺序收到全部 $N$ 个礼物后，他最终的情绪值是多少？&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 10000$&lt;/li&gt;
&lt;li&gt;$1 \leq P_i \leq 500$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i \leq 500$&lt;/li&gt;
&lt;li&gt;$1 \leq B_i \leq 500$&lt;/li&gt;
&lt;li&gt;$1 \leq Q \leq 5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq X_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $N$ 行，每行包含三个整数 $P_i$ 、$A_i$ 和 $B_i$ 。&lt;/li&gt;
&lt;li&gt;接下来一行包含一个整数 $Q$ 。&lt;/li&gt;
&lt;li&gt;接下来 $Q$ 行，每行包含一个整数 $X_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$P_1 \quad A_1 \quad B_1$&lt;/p&gt;
&lt;p&gt;$P_2 \quad A_2 \quad B_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$P_N \quad A_N \quad B_N$&lt;/p&gt;
&lt;p&gt;$Q$&lt;/p&gt;
&lt;p&gt;$X_1$&lt;/p&gt;
&lt;p&gt;$X_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$X_Q$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $Q$ 行，每行包含一个整数，表示对应询问的最终情绪值。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
3 1 4
1 5 9
2 6 5
3 5 8
11
0
1
2
3
4
5
6
7
8
9
10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
0
0
0
5
6
0
0
0
0
0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;本题的核心挑战在于处理极大量的询问 $Q = 5 \times 10^5$ ，这使得常规的 $O(NQ)$ 模拟无法在时限内完成。解题的关键突破口在于挖掘题目给出的参数限制：由于礼物的价值 $P_i$ 、情绪增加量 $A_i$ 和减少量 $B_i$ 均不超过 $500$ ，高桥君的情绪值在变化过程中表现出明显的收敛特性。当初始情绪值 $X$ 远大于 $500$ 时，他会因为眼光过高而对收到的礼物持续感到失望，导致情绪值呈线性下降趋势。这种下降会一直持续，&lt;strong&gt;直到情绪值跌入特定的波动区间&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;我们可以定义一个临界值 $M = 1000$ 。只要当前情绪值高于这个临界值，由于 $P_i \leq 500$ 的限制，高桥君必然会触发 &lt;strong&gt;情绪减少 $B_i$&lt;/strong&gt; 的逻辑。只有当情绪值降低到 $500$ 以下时，才可能因为 $X \leq P_i$ 而触发 &lt;strong&gt;情绪增加 $A_i$&lt;/strong&gt; 的逻辑。这里有一个非常重要的性质：情绪值一旦进入 $[0, 1000]$ 区间，就永远不会再超过 $1000$ 。这是因为触发增加逻辑的前提是 $j \leq P_i \leq 500$ ，即便加上最大的增加量 $A_i \leq 500$ ，结果也恰好维持在 $1000$ 。&lt;/p&gt;
&lt;p&gt;针对临界区间内的变动，我们利用动态规划进行预处理。设 $dp[i][j]$ 表示在处理第 $i$ 个礼物前，当前情绪值为 $j$ ，在处理完剩余所有礼物后最终的情绪值。我们可以通过逆向推导来填充这个动态规划表：从最后一个礼物 $N$ 开始往前考虑，根据当前礼物 $i$ 的参数计算出处理完该礼物后的新情绪值 $j&apos;$ 。&lt;/p&gt;
&lt;p&gt;$$
j&apos; = \begin{cases} j + A_i &amp;amp; \text{if } j \leq P_i \ \max(0, j - B_i) &amp;amp; \text{if } j &amp;gt; P_i \end{cases}
$$&lt;/p&gt;
&lt;p&gt;由于上述性质保证了 $j&apos; \leq 1000$ ，状态转移可以简单地写为：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = dp[i+1][j&apos;]
$$&lt;/p&gt;
&lt;p&gt;对于每个具体的询问 $X_i$ ，我们可以通过二分查找快速定位满足如下约束条件的最小下标 $k$：&lt;/p&gt;
&lt;p&gt;$$
X_i - sumB[k] \leq 1000
$$&lt;/p&gt;
&lt;p&gt;若在处理完所有礼物前找到了这个 $k$ ，则说明情绪值在第 $k$ 个礼物处跌入了预处理区间。此时，该询问的最终答案直接由 $dp[k+1][\max(0, X_i - sumB[k])]$ 给出。如果直到最后礼物领完，情绪值依然高于 $1000$ ，答案则是 $X_i - sumB[N]$ 。这种方法将查询复杂度降至 $O(\log N)$ ，能够完美解决大规模询问下的时限挑战。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
int N, Q;
int dp[10005][1005];
int P[10005], A[10005], B[10005];
long long preB[10005];

int main() {
    cin &amp;gt;&amp;gt; N;
    for (int i = 0; i &amp;lt; N; i++) {
        cin &amp;gt;&amp;gt; P[i] &amp;gt;&amp;gt; A[i] &amp;gt;&amp;gt; B[i];
        preB[i + 1] = preB[i] + B[i];
    }

    // 逆向 DP 预处理
    for (int j = 0; j &amp;lt;= 1000; j++) {
        dp[N][j] = j;
    }
    for (int i = N - 1; i &amp;gt;= 0; i--) {
        for (int j = 0; j &amp;lt;= 1000; j++) {
            int next_j;
            if (P[i] &amp;gt;= j) {
                next_j = j + A[i];
            } else {
                next_j = max(0, j - B[i]);
            }
            dp[i][j] = dp[i + 1][next_j];
        }
    }

    cin &amp;gt;&amp;gt; Q;
    while (Q--) {
        ll x; cin &amp;gt;&amp;gt; x;
        int k = lower_bound(preB, preB + N + 1, x - 1000) - preB;
        
        if (k &amp;gt; N) {
            cout &amp;lt;&amp;lt; x - preB[N] &amp;lt;&amp;lt; endl;
        } else {
            ll val = max(0LL, x - preB[k]);
            cout &amp;lt;&amp;lt; dp[k][(int)val] &amp;lt;&amp;lt; endl;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数字的生成游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1132&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/enum-optim/rectangle-problem/&quot;&gt;【ACM 算法题单】矩形相关问题&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基础算法】第五节：谱聚类算法</title><link>https://xingguang641.com/posts/spectral-clustering/spectral-clustering/</link><guid isPermaLink="true">https://xingguang641.com/posts/spectral-clustering/spectral-clustering/</guid><description>介绍机器学习常见的算法</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【机器学习基本模型】第十二节：玻尔兹曼机</title><link>https://xingguang641.com/posts/boltzmann-machine/boltzmann-machine/</link><guid isPermaLink="true">https://xingguang641.com/posts/boltzmann-machine/boltzmann-machine/</guid><description>介绍机器学习常见的模型</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【深度学习基本模型】特别篇：Pytorch 框架</title><link>https://xingguang641.com/posts/pytorch/pytorch/</link><guid isPermaLink="true">https://xingguang641.com/posts/pytorch/pytorch/</guid><description>介绍深度学习常见的模型</description><pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【线代的本质】第一节：向量的本质</title><link>https://xingguang641.com/posts/math/linear-algebra/vectors-essence/vectors-essence/</link><guid isPermaLink="true">https://xingguang641.com/posts/math/linear-algebra/vectors-essence/vectors-essence/</guid><description>从可视化角度详细讲解线代的本质</description><pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【深度学习基本模型】第二节：Transformer</title><link>https://xingguang641.com/posts/transformer/transformer/</link><guid isPermaLink="true">https://xingguang641.com/posts/transformer/transformer/</guid><description>介绍深度学习常见的模型</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded/></item><item><title>【ACM 算法题单】矩形相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/enum-optim/rectangle-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/enum-optim/rectangle-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;矩形累加和问题&lt;/h1&gt;
&lt;p&gt;矩形累加和是矩形类问题中最基础、也最常见的一类题型。如果我们要在二维矩阵中寻找累加和最大的子矩形，最直接的思路是枚举矩形的左上角和右下角，从而确定出一个子矩形并计算其元素之和。然而在这种朴素做法中，整体枚举复杂度高达 $O(n^4)$ ，在实际题目中往往是 &lt;strong&gt;无法接受的&lt;/strong&gt; 。为了降低复杂度，我们不再同时枚举四条边，而是只枚举矩形的上下边界，然后将这两条边之间的所有行压缩到一维数组中。这样一来，原本的二维矩形问题就被转化为一维区间问题。&lt;/p&gt;
&lt;p&gt;在完成压缩之后，问题的本质便转变为 &lt;strong&gt;在一个一维数组中寻找满足题目条件的子区间&lt;/strong&gt; 。此时，我们可以直接套用各种成熟的一维算法，例如前缀和、哈希表、双指针或最大子段和等经典方法，从而高效地求解矩形的左右边界。通过这种典型的 &lt;strong&gt;二维转一维&lt;/strong&gt; 技巧，我们成功地将整体时间复杂度优化到了 $O(n^3)$ ，这一复杂度在大多数矩形累加和相关问题中都足够高效，也构成了此类题目的 &lt;strong&gt;核心解题框架&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;子矩阵的最大和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/max-submatrix-lcci&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个正整数、负整数和 $0$ 组成的 $N × M$ 矩阵，编写代码找出元素总和最大的子矩阵。&lt;/p&gt;
&lt;p&gt;返回一个数组 &lt;code&gt;[r1, c1, r2, c2]&lt;/code&gt; ，其中 &lt;code&gt;r1, c1&lt;/code&gt; 分别代表子矩阵左上角的行号和列号，&lt;code&gt;r2, c2&lt;/code&gt; 分别代表右下角的行号和列号。若有多个满足条件的子矩阵，返回任意一个均可。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq matrix.length \leq 200$&lt;/li&gt;
&lt;li&gt;$1 \leq matrix[0].length \leq 200$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ ，表示矩阵大小。&lt;/li&gt;
&lt;li&gt;接下来 $M$ 行包含 $N$ 个整数，表示矩阵的一行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个四元组表示答案矩阵的坐标。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
-1 0
0 -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 1 0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;均衡的矩形计数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc410/tasks/abc410_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个 $H \times W$ 的网格，每个单元格包含 &lt;code&gt;#&lt;/code&gt; 或 &lt;code&gt;.&lt;/code&gt; 。每个单元格中的符号信息由 $H$ 个长度为 $W$ 的字符串 $S_1, S_2, \ldots, S_H$ 给出，其中第 $i$ 行第 $j$ 列的单元格包含与 $S_i$ 的第 $j$ 个字符相同的符号。&lt;/p&gt;
&lt;p&gt;找出满足以下所有条件的矩形区域的数量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;矩形区域内包含 &lt;code&gt;#&lt;/code&gt; 的单元格数量和包含 &lt;code&gt;.&lt;/code&gt; 的单元格数量相等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正式地，找出满足以下所有条件的整数四元组 $(u, d, l, r)$ 的数量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq u \leq d \leq H$&lt;/li&gt;
&lt;li&gt;$1 \leq l \leq r \leq W$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当从第 $u$ 行到第 $d$ 行以及从第 $l$ 列到第 $r$ 列提取网格的一部分时，提取部分中包含 &lt;code&gt;#&lt;/code&gt; 的单元格数量和包含 &lt;code&gt;.&lt;/code&gt; 的单元格数量相等。&lt;/p&gt;
&lt;p&gt;你有 $T$ 个测试用例。对每个测试用例找出答案。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 25000$&lt;/li&gt;
&lt;li&gt;$1 \leq H, W$&lt;/li&gt;
&lt;li&gt;所有测试用例中 $H \times W$ 的总和不超过 $3 \times 10^5$&lt;/li&gt;
&lt;li&gt;$S_i$ 是长度为 $W$ 的由 &lt;code&gt;#&lt;/code&gt; 和 &lt;code&gt;.&lt;/code&gt; 组成的字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$case_1$&lt;/p&gt;
&lt;p&gt;$case_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$case_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $H$ 和 $W$ ，表示网格的大小。&lt;/li&gt;
&lt;li&gt;接下来的 $H$ 行，每行包含一个长度为 $W$ 的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$H \quad W$&lt;/p&gt;
&lt;p&gt;$S_1$&lt;/p&gt;
&lt;p&gt;$S_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$S_H$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $T$ 行，第 $i$ 行应该包含第 $i$ 个测试用例的答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
3 2
##
#.
..
6 6
..#...
..#..#
#.#.#.
.###..
######
.###..
15 50
.......................#...........###.###.###.###
....................#..#..#..........#.#.#...#.#..
.................#...#####...#.....###.#.#.###.###
..................#..##.##..#......#...#.#.#.....#
...................#########.......###.###.###.###
....................#.....#.......................
.###........##......#.....#..#...#.####.####.##..#
#..#.........#......#.....#..#...#.#....#....##..#
#..#.........#......#.....#..#...#.#....#....##..#
#.....##...###..##..#.....#..#...#.#....#....#.#.#
#....#..#.#..#.#..#.#..##.#..#...#.####.####.#.#.#
#....#..#.#..#.####.#....##..#...#.#....#....#.#.#
#....#..#.#..#.#....#.....#..#...#.#....#....#..##
#..#.#..#.#..#.#..#.#....#.#.#...#.#....#....#..##
.##...##...####.##...####..#..###..####.####.#..##
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
79
4032
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;完全子矩形问题&lt;/h1&gt;
&lt;p&gt;完全子矩形（元素全为 $1$ 的矩形）是矩形类问题中第二类典型题型。与前一种矩形累加和问题相同，如果直接通过枚举子矩形的左上角和右下角来确定一个矩形，其时间复杂度会非常高，在数据规模稍大的情况下难以通过，因此我们需要找到新的枚举思路。不同于矩形累加和问题，完全子矩形问题不需要枚举矩形的上下边界，只需要单独 &lt;strong&gt;枚举矩形的底边&lt;/strong&gt; 即可。以当前行为底边向上延伸，我们可以统计出每一列中连续为 $1$ 的高度，从而将原本的二维矩形问题转化为一维柱状图问题。此时，问题等价于 &lt;strong&gt;「柱状图中最大的矩形」&lt;/strong&gt; 这一经典题目。&lt;/p&gt;
&lt;p&gt;由于「柱状图中最大的矩形」这一经典问题可以借助单调栈在 $O(n)$ 的时间内高效解决，当我们将矩阵中的每一行依次视为矩形的底边并进行处理时，整体时间复杂度便可以稳定地控制在 $O(n^2)$ 。在这一过程中，二维矩形问题被系统性地转化为一维柱状图问题，从而利用「柱状图中最大的矩形」这一成熟的算法工具。这种处理方式有效避免了对矩形四条边进行高维度暴力枚举，是解决完全子矩形问题时最为常见、也最为标准的思路。&lt;/p&gt;
&lt;h2&gt;柱状图最大矩形&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/largest-rectangle-in-histogram/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定 $n$ 个非负整数，用来表示柱状图中各个柱子的高度。每个柱子彼此相邻，且宽度为 $1$ 。&lt;/p&gt;
&lt;p&gt;求在该柱状图中，能够勾勒出来的矩形的最大面积。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq heights.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq heights[i] \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$heights_1 \quad heights_2 \quad \ldots \quad heights_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
2 1 5 6 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;最大的全一矩形&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/PLYXKQ/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个由 $0$ 和 $1$ 组成的矩阵 &lt;code&gt;matrix&lt;/code&gt; ，找出只包含 $1$ 的最大矩形，并返回其面积。&lt;/p&gt;
&lt;p&gt;注意：此题 &lt;code&gt;matrix&lt;/code&gt; 输入格式为一维 $01$ 字符串数组。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$rows == matrix.length$&lt;/li&gt;
&lt;li&gt;$cols == matrix[0].length$&lt;/li&gt;
&lt;li&gt;$0 &amp;lt;= row, cols &amp;lt;= 200$&lt;/li&gt;
&lt;li&gt;$matrix[i][j]$ 仅包含 $0$ 或 $1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示矩阵的行数。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行包含一个字符串，表示矩阵的一行。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$S_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$S_2$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
10100
10111
11111
10010
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;全一矩形的个数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-submatrices-with-all-ones/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个 &lt;code&gt;m x n&lt;/code&gt; 的二进制矩阵 &lt;code&gt;mat&lt;/code&gt; ，请你返回有多少个 &lt;strong&gt;子矩形&lt;/strong&gt; 的元素全部都是 $1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq m, n \leq 150$&lt;/li&gt;
&lt;li&gt;$mat[i][j]$ 仅包含 $0$ 或 $1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $m$ 和 $n$ ，表示矩阵大小。&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行包含 $n$ 个整数，表示矩阵的一行。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$m \quad n$&lt;/p&gt;
&lt;p&gt;$mat_{1, 1} \quad mat_{1, 2} \quad \ldots \quad mat_{1, n}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$mat_{h, 1} \quad mat_{h, 2} \quad \ldots \quad mat_{m, n}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
1 0 1
1 1 0
1 1 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;13
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 4
0 1 1 0
0 1 1 1
1 1 1 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;24
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;寻找高光的片段&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc420/tasks/abc420_f&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个 $N \times M$ 的网格。每个单元格包含 &lt;code&gt;.&lt;/code&gt; 或 &lt;code&gt;#&lt;/code&gt; 。每个单元格中的符号信息由 $N$ 个字符串 $S_1, S_2, \ldots, S_N$ 给出，其中第 $i$ 行第 $j$ 列的单元格包含与 $S_i$ 的第 $j$ 个字符相同的符号。有多少个最多包含 $K$ 个单元格的矩形区域，使得所有单元格都包含 &lt;code&gt;.&lt;/code&gt; ？&lt;/p&gt;
&lt;p&gt;正式地，计算满足以下条件的整数四元组 $(l_x, r_x, l_y, r_y)$ 的数量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq l_x \leq r_x \leq N$&lt;/li&gt;
&lt;li&gt;$1 \leq l_y \leq r_y \leq M$&lt;/li&gt;
&lt;li&gt;$(r_x - l_x + 1) \times (r_y - l_y + 1) \leq K$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于所有满足 $l_x \leq i \leq r_x$ 且 $l_y \leq j \leq r_y$ 的整数对 $(i, j)$ ，第 $i$ 行第 $j$ 列的单元格包含 &lt;code&gt;.&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$N, M, K$ 是整数。&lt;/li&gt;
&lt;li&gt;$1 \leq N, M \leq 5 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq N \times M \leq 5 \times 10^6$&lt;/li&gt;
&lt;li&gt;$1 \leq K \leq N \times M$&lt;/li&gt;
&lt;li&gt;$S_i$ 是长度为 $M$ 且由 &lt;code&gt;.&lt;/code&gt; 和 &lt;code&gt;#&lt;/code&gt; 组成的字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$M$ 和 $K$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $N$ 行，每行包含一个长度为 $M$ 的字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M \quad K$&lt;/p&gt;
&lt;p&gt;$S_1$&lt;/p&gt;
&lt;p&gt;$S_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$S_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3 4
#..
...
..#
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;19
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 5 35
.....
.....
.....
.....
.....
.....
.....
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;420
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10 9 25
#.....#..
....#....
.......#.
.........
.......#.
.#.......
.........
#........
........#
.#.....#.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;984
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
</content:encoded></item><item><title>【ACM 算法题单】MEX相关问题</title><link>https://xingguang641.com/posts/acm/acm-type/math-operators/mex-problem/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-type/math-operators/mex-problem/</guid><description>记录一些 ACM 常见题型</description><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;MEX区间构造问题&lt;/h1&gt;
&lt;p&gt;MEX 描述的是某个区间内 &lt;strong&gt;最小的未出现的非负整数&lt;/strong&gt; 。从定义出发，如果一个区间的 MEX 等于 $x$ ，那么这个区间必须 &lt;strong&gt;完整包含&lt;/strong&gt; 所有整数 $0, 1, 2, \dots, x-1$ ，否则更小的缺失值会优先成为 MEX；与此同时，整数 $x$ &lt;strong&gt;不能出现在该区间中&lt;/strong&gt; ，否则 MEX 会被进一步推大。这两个条件，是解决所有 MEX 问题的核心约束。&lt;/p&gt;
&lt;p&gt;进一步地，从 &lt;strong&gt;区间长度的角度&lt;/strong&gt; 我们可以发现一个重要的结论：对于一个长度为 $n$ 的区间，由于其中最多只能包含 $n$ 个不同的数，因此不可能同时覆盖 $0 \sim n$ 这 $n+1$ 个整数，也就是说 MEX 的取值一定 &lt;strong&gt;不超过 n&lt;/strong&gt; 。这一结论说明 MEX 的取值范围与区间长度直接关联，这为后续的枚举、构造以及状态设计提供了重要的 &lt;strong&gt;上界控制&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;MAC中的信息&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/contest/1935/problem/B&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个包含 $n$ 个非负整数的数组 $a$ 。&lt;/p&gt;
&lt;p&gt;你需要将该数组分割成 $k \geq 2$ 个连续的子数组，使得每个子数组的 MEX 值都 &lt;strong&gt;相等&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果存在这样的分割方案，请输出一种；否则，输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq t \leq 10^4$&lt;/li&gt;
&lt;li&gt;$2 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq a_i &amp;lt; n$&lt;/li&gt;
&lt;li&gt;所有测试用例中 $n$ 的总和不超过 $2 \times 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一行包含一个整数 $t$ ，表示测试用例的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数 $a_1, a_2, \ldots, a_n$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$t$&lt;/p&gt;
&lt;p&gt;$n_1$&lt;/p&gt;
&lt;p&gt;$a_{11} \quad a_{12} \quad \ldots \quad a_{1n_1}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$n_t$&lt;/p&gt;
&lt;p&gt;$a_{t1} \quad a_{t2} \quad \ldots \quad a_{tn_t}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不存在满足条件的分割方案，输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;否则，第一行输出一个整数 $k$ ，表示分割的子数组数量。&lt;/li&gt;
&lt;li&gt;接下来 $k$ 行，每行包含两个整数 $l_i, r_i$ ，表示第 $i$ 个子数组的左端点和右端点。&lt;/li&gt;
&lt;li&gt;必须满足 $l_1 = 1, r_k = n$ ，且对于所有 $1 \leq i &amp;lt; k$ ，有 $l_{i+1} = r_i + 1$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
2
0 0
5
0 1 2 3 4
8
0 1 7 1 0 1 0 3
3
1 1 1
10
0 5 0 3 0 1 3 2 2 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
1 1
2 2
-1
3
1 3
4 5
6 8
3
1 1
2 2
3 3
-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;MEX区间查询问题&lt;/h1&gt;
&lt;p&gt;我们暂时不讲解复杂的区间查询问题，而是思考这么一个基础任务：如何维护一个集合的 MEX，并且支持集合元素的 &lt;strong&gt;动态插入与删除&lt;/strong&gt; 。在这种场景下，集合会随着操作实时发生变化，我们的目标是在每次插入或删除元素之后，能够 &lt;strong&gt;快速得到当前集合的 MEX 值&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;一种常见且直观的做法是维护当前集合中所有未出现的数。具体而言，可以使用一个 &lt;code&gt;set&lt;/code&gt; 来存储这些数字。初始时，将 $0 \sim n$ 所有可能的数加入 &lt;code&gt;set&lt;/code&gt; ，表示这些数最开始都没有出现在集合中。当向集合中加入一个数 $x$ 时，如果它原本在 &lt;code&gt;set&lt;/code&gt; 中，则将其删除；当从集合中删除一个数 $x$ 时，如果该数的计数降到 $0$ ，则重新将它加入 &lt;code&gt;set&lt;/code&gt; 。在实际实现中，通常需要配合一个 &lt;strong&gt;计数数组&lt;/strong&gt; $cnt[x]$ 来记录每个数字在集合中的出现次数，只有当某个数字的计数从 $0$ 变为 $1$ ，或从 $1$ 变为 $0$ 时，才对 &lt;code&gt;set&lt;/code&gt; 进行相应的更新，以确保维护过程的正确性和一致性。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;set&lt;/code&gt; 中始终保存的是当前集合中未出现的数字，因此它的 &lt;strong&gt;最小元素&lt;/strong&gt; 自然就是当前集合的 MEX。考虑到 &lt;code&gt;set&lt;/code&gt; 的插入、删除以及获取最小值操作的时间复杂度均为 $O(\log n)$ ，如果集合总共经历 $q$ 次操作，那么维护整个过程的 &lt;strong&gt;总体时间复杂度为 $O(q \log n)$&lt;/strong&gt; ，可以高效地动态维护集合的 MEX。&lt;/p&gt;
&lt;h3&gt;任意区间的离线查询（一）&lt;/h3&gt;
&lt;p&gt;在讲解完上面这个基础问题后，我们可以正式引入 MEX 区间查询的基础模型：对于一个静态数组，查询所有区间的 MEX 值。若采用暴力做法，我们可以枚举每一个左端点 $L$ ，然后让右端点 $R$ 从左到右移动。在这个过程中可以发现，随着 $R$ 的增加，区间的 MEX 是 &lt;strong&gt;单调不减&lt;/strong&gt; 的，因此对于每个 $L$ ，可以在 $O(n)$ 的时间内求出所有对应的区间 MEX。对所有 $L$ 执行这一过程，总时间复杂度为 $O(n^2)$ ，本质上等价于枚举所有子数组。&lt;/p&gt;
&lt;p&gt;上述做法的问题在于，不同左端点之间的信息是完全割裂的，导致出现大量的重复计算。实际上，当我们已经处理完 $L=i$ 的所有区间后，这些信息在转移到 $L=i+1$ 时并不会全部失效。将左端点从 $i$ 移动到 $i+1$ ，本质上只是从区间中删除了一个元素 $a[i]$ ，因此我们只需要分析这个删除的元素对已有 MEX 结果造成什么样的影响即可，而不需要重新计算整个区间。&lt;/p&gt;
&lt;p&gt;为此，我们可以预处理一个 $next$ 数组，用来记录每个位置的数在右侧 &lt;strong&gt;第一次再次出现&lt;/strong&gt; 的位置。当左端点从 $i$ 移动到 $i+1$ 时，对于右端点 $R \geq next[i]$ 的区间，由于区间内仍然包含一个 $a[i]$ ，因此 MEX 不会发生变化；只有当 $R &amp;lt; next[i]$ 时，区间内部才会真正失去了这个数，从而影响这个区间的 MEX，并且只有当原本的 MEX 大于 $a[i]$ 时，删除该数才会改变结果。结合 MEX 随 $R$ 单调不减的性质，可以在当前 MEX 数组中二分找到第一个大于 $a[i]$ 的位置 $k$ ，并将所有左端点 $L=i$ 、右端点 $R \in [k, next[i]-1]$ 的区间 MEX &lt;strong&gt;统一修改为 $a[i]$&lt;/strong&gt; ，从而完成从 $L=i$ 到 $L=i+1$ 的高效转移。&lt;/p&gt;
&lt;p&gt;因此，我们定义一个数组 $mex$ ，其中 $mex[i]$ 表示区间 $[0, i]$ 的 MEX 值，并使用线段树对该数组进行维护。在完成初始状态的构建后，接下来考虑如何将左端点从 $L=i$ 转移到 $L=i+1$ 。根据前面的分析，这一过程只会影响一段连续区间的取值，因此我们可以在线段树上进行二分找到第一个满足 $mex[k] &amp;gt; a[i]$ 的位置 $k$ ，并对区间 $[k, next[i]-1]$ 进行 &lt;strong&gt;区间赋值更新&lt;/strong&gt; ，将其统一修改为 $a[i]$ 。通过这样的区间修改，即可在对数时间内完成一次转移，总体时间复杂度为 $O(n \log n)$ ，优于一般的暴力解法。&lt;/p&gt;
&lt;h3&gt;任意区间的离线查询（二）&lt;/h3&gt;
&lt;p&gt;我们换一种思路来处理上面这个 MEX 区间查询问题。对于一个区间 $[L, R]$ ，其 MEX 本质上是 &lt;strong&gt;寻找一个最小的非负整数 x，使得 x 在该区间内从未出现过&lt;/strong&gt; 。据此，我们可以固定右端点 $R$ ，通过左端点 $L$ 进行查询判断。为辅助判断，我们引入一个 $last$ 数组，其中 $last[x]$ 表示数字 $x$ 最近一次出现的位置。这样，区间的 MEX 查询就转化为寻找最小的 $x$ ，使得 $last[x] &amp;lt; L$ 。然而，由于原数组不是有序的，$last$ 数组也不具备有序性，如果对每个查询都要遍历整个 $last$ 数组，就会导致时间复杂度达到 $O(n^2)$ ，显然效率不高，因此需要进一步优化。&lt;/p&gt;
&lt;p&gt;我们考虑使用权值线段树来维护这个 $last$ 数组。线段树的下标对应 &lt;strong&gt;数值 x（即值域）&lt;/strong&gt;，每个叶子节点存储该数字的 $last[x]$ ，表示它最近一次出现的位置；&lt;strong&gt;由于我们要查找小于 L 的 last 值，每个父节点维护其区间内的最小 last 值&lt;/strong&gt; 。这样，当我们查询某个左端点 $L$ 时，只需从根节点开始搜索：如果当前节点的 $last_{min} \geq L$ ，说明该节点管理的所有数字都在区间 $[L, R]$ 内出现过，可以直接跳过；否则优先递归访问左子树，确保能够找到 &lt;strong&gt;最小的 x&lt;/strong&gt; ，使得 $last[x] &amp;lt; L$ 。&lt;/p&gt;
&lt;p&gt;我们考虑按 $R$ 的大小来处理查询，并利用&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/two-sum-idea/two-sum-idea/&quot;&gt;两数之和&lt;/a&gt;的思想，枚举右端点 $R$ ，同时维护左端点 $L$ 。当右端点向右移动，遇到新的数字 $a[R]$ 时，只需将对应叶子节点的 $last[a[R]]$ 更新为 $R$ ，并向上更新父节点的最小 $last$ 值。通过这种方式，每次右端点移动时的更新，以及对左端点 $L$ 的查询，都可以在 $O(\log n)$ 时间内完成，从而大幅提升整体查询效率。&lt;/p&gt;
&lt;h3&gt;任意区间的在线查询&lt;/h3&gt;
&lt;p&gt;需要用到主席树&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;MEX区间计数问题&lt;/h1&gt;
&lt;p&gt;需要用到极小mex区间（比较困难）（两数之和思想）&lt;/p&gt;
&lt;p&gt;还有一个简单的差分方法（比较简单）&lt;/p&gt;
&lt;p&gt;有可能有On解法，先暂时放着&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/563177000&quot;&gt;【知乎专栏】求区间 MEX 的多种方法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://entiesci.github.io/oi-beats/site/%E6%95%B0%E5%AD%A6/MEX/&quot;&gt;【OI Beats】MEX 相关题目合集&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/little-corn/p/18722880&quot;&gt;【Little_corn】关于 MEX 的几个问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/BigSmall-En/p/16526110.html&quot;&gt;【BigSmall_En】与 MEX 有关的题目&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/4cu6e946&quot;&gt;【Luogu 博客】浅谈「极小 MEX 区间」问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【开源项目部署教程】RAGFlow项目教程</title><link>https://xingguang641.com/posts/github/github-project/rag-flow/</link><guid isPermaLink="true">https://xingguang641.com/posts/github/github-project/rag-flow/</guid><description>基于 Docker Compose 的 RAGFlow 独立部署教程</description><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇博客教程来自于该视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113950951736220&amp;amp;bvid=BV1WiP2ezE5a&amp;amp;cid=28278981043&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h1&gt;Ollama模型配置&lt;/h1&gt;
&lt;p&gt;在配置 &lt;strong&gt;RAGFlow&lt;/strong&gt; 之前，第一步是准备好本地的 &lt;strong&gt;大语言模型（LLM）&lt;/strong&gt;，而 &lt;strong&gt;Ollama&lt;/strong&gt; 是一个非常方便的工具，可以用来在本地运行和管理这些模型。首先，需要在本机上下载并安装 Ollama 平台。安装完成后，接下来就是对环境进行一些必要的配置，以保证虚拟机中的 RAGFlow 可以顺利访问到本地模型。&lt;/p&gt;
&lt;p&gt;在配置过程中，有两个环境变量需要特别注意。第一个是 &lt;code&gt;OLLAMA_HOST&lt;/code&gt; ，通常设置为 &lt;code&gt;0.0.0.0:11434&lt;/code&gt; 。这个设置的作用是让虚拟机可以通过网络访问本机上的 Ollama 服务。如果配置完成后虚拟机无法访问，很可能是本机防火墙拦截了 11434 端口。此时，如果不想直接开放这个端口，也可以通过 &lt;strong&gt;SSH 端口转发&lt;/strong&gt; 来实现访问。完成环境变量配置后，记得重启系统或者终端，使设置生效。&lt;/p&gt;
&lt;p&gt;另一个需要配置的环境变量是 &lt;code&gt;OLLAMA_MODELS&lt;/code&gt; ，它用于指定模型存储位置。Ollama 默认会将下载的模型放在 C 盘，如果你希望将模型保存到其他盘符或自定义目录，需要通过该环境变量进行调整。&lt;/p&gt;
&lt;p&gt;在环境配置完成之后，就可以通过 Ollama 下载所需的模型了。以 DeepSeek 大模型为例，命令非常简单，只需执行 &lt;code&gt;ollama run deepseek-r1:1.5b&lt;/code&gt; ，Ollama 就会自动下载并启动该模型，准备好供 RAGFlow 使用。通过这一系列步骤，你就完成了本地 LLM 的准备工作，为后续的 RAGFlow 配置打下了基础。&lt;/p&gt;
&lt;h1&gt;RAGF项目部署&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;RAGFlow&lt;/strong&gt; 是一个集成本地大语言模型与知识检索的问答系统，能够将本地大模型与外部知识库结合，实现智能问答、内容生成和信息检索的统一管理。它可以直接在本地 Python 环境中运行，但为了保证环境隔离、依赖一致性以及后续维护的便利，通常推荐使用 &lt;strong&gt;Docker 容器化部署&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过 Docker 部署 RAGFlow，可以在独立的容器中运行所有服务，避免因系统环境或依赖冲突导致的问题。同时，容器化也方便对不同版本的模型和服务进行管理和更新，实现快速启动、易于备份和迁移。对于个人用户来说，这种方式能够快速搭建一个可用的本地问答环境；对于团队或服务器环境，则能够保证服务的稳定性和可扩展性。&lt;/p&gt;
&lt;p&gt;在容器化环境中，RAGFlow 可以直接连接本地已配置好的 Ollama 服务和 DeepSeek 模型，实现对模型的调用和管理。借助 Docker Compose 或类似工具，还可以一次性启动数据库、RAGFlow 服务以及其他依赖组件，形成一个完整的、独立可运行的问答系统。这种部署方式不仅提高了系统稳定性，还方便在不同机器或环境间迁移，极大地降低了运维成本。&lt;/p&gt;
&lt;h2&gt;获取项目代码&lt;/h2&gt;
&lt;p&gt;项目开源地址如下：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;infiniflow/ragflow&quot;}&lt;/p&gt;
&lt;p&gt;首先使用 Git 将项目克隆到本地：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/infiniflow/ragflow.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置Docker&lt;/h2&gt;
&lt;p&gt;进入 &lt;strong&gt;docker&lt;/strong&gt; 文件夹，利用官方提前构建好的 Docker 镜像即可启动 RAGFlow 服务器。&lt;/p&gt;
&lt;p&gt;:::warning
⚠️ &lt;strong&gt;注意&lt;/strong&gt;：当前官方提供的 Docker 镜像均基于 &lt;strong&gt;x86 架构&lt;/strong&gt; 构建，不提供 ARM64 架构的镜像。如果你的操作系统是 ARM64 架构，请参考&lt;a href=&quot;https://ragflow.io/docs/dev/build_docker_image&quot;&gt;官方文档&lt;/a&gt;自行构建 Docker 镜像。
:::&lt;/p&gt;
&lt;p&gt;运行以下命令会自动下载并启动 &lt;strong&gt;RAGFlow Docker 镜像 &lt;code&gt;v0.22.1&lt;/code&gt;&lt;/strong&gt; 。如果需要使用其他版本的镜像，请在运行 &lt;code&gt;docker compose&lt;/code&gt; 之前，先更新 &lt;strong&gt;docker/.env&lt;/strong&gt; 文件中的 &lt;code&gt;RAGFLOW_IMAGE&lt;/code&gt; 变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd ragflow/docker

# 可选：切换到稳定版本标签（查看发布记录：https://github.com/infiniflow/ragflow/releases）
# git checkout v0.22.1
# 这一步确保代码中的 entrypoint.sh 文件与 Docker 镜像版本保持一致

# 使用 CPU 执行 DeepDoc 任务
$ docker compose -f docker-compose.yml up -d

# 使用 GPU 加速 DeepDoc 任务（需在 .env 文件首行添加 DEVICE=gpu）
# sed -i &apos;1i DEVICE=gpu&apos; .env
# docker compose -f docker-compose.yml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;strong&gt;镜像版本说明&lt;/strong&gt;：&lt;code&gt;v0.22.0&lt;/code&gt; 之前的版本，官方提供了两种镜像：包含 embedding 模型的完整镜像和不含 embedding 模型的 slim 镜像。
:::&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RAGFlow image tag&lt;/th&gt;
&lt;th&gt;镜像大小 (GB)&lt;/th&gt;
&lt;th&gt;是否包含 embedding 模型&lt;/th&gt;
&lt;th&gt;稳定性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v0.21.1&lt;/td&gt;
&lt;td&gt;≈9&lt;/td&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;稳定版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v0.21.1-slim&lt;/td&gt;
&lt;td&gt;≈2&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;稳定版本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;通过以上步骤，即可在 Docker 环境中快速启动 RAGFlow 服务，并可根据实际需求灵活选择 CPU 或 GPU 运行模式。同时该方式能够确保服务配置与镜像版本保持一致，便于后续的升级、使用与维护。需要注意的是，在访问服务时请关闭代理，否则可能导致服务无法正常访问。&lt;/p&gt;
&lt;h2&gt;访问RAGFlow&lt;/h2&gt;
&lt;p&gt;完成容器化部署后，你可以通过浏览器或 API 直接访问 RAGFlow 服务。Docker Compose 会将 RAGFlow 的 Web 服务端口映射到本地环境中，常用端口如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP&lt;/strong&gt;：&lt;code&gt;${SVR_WEB_HTTP_PORT}&lt;/code&gt;（默认 80）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;：&lt;code&gt;${SVR_WEB_HTTPS_PORT}&lt;/code&gt;（默认 443）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin Server&lt;/strong&gt;：&lt;code&gt;${ADMIN_SVR_HTTP_PORT}&lt;/code&gt;（默认 9381）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，在浏览器中访问 &lt;code&gt;http://localhost:${SVR_WEB_HTTP_PORT}&lt;/code&gt; 就可以打开 RAGFlow 主界面；若启用了 HTTPS，则可使用 &lt;code&gt;https://localhost:${SVR_WEB_HTTPS_PORT}&lt;/code&gt; 进行访问。&lt;/p&gt;
</content:encoded></item><item><title>【深度学习笔记】过拟合现象与正则化方法</title><link>https://xingguang641.com/posts/dl-note/regularization/</link><guid isPermaLink="true">https://xingguang641.com/posts/dl-note/regularization/</guid><description>深度探究过拟合现象与正则化方法</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;参考文献&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sm1les.com/2019/01/07/l1-and-l2-regularization/&quot;&gt;L1正则化比L2正则化易得稀疏解的三种解释&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/pdf/1711.05101&quot;&gt;Decoupled Weight Decay Regularization&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://benihime91.github.io/blog/machinelearning/deeplearning/python3.x/tensorflow2.x/2020/10/08/adamW.html&quot;&gt;L2 Regularization and Weight Decay&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】单调数据结构的应用</title><link>https://xingguang641.com/posts/acm/acm-note/monotonic-structure/monotonic-structure/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/monotonic-structure/monotonic-structure/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 27 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单调数据结构介绍&lt;/h1&gt;
&lt;p&gt;在设计算法时，识别并利用题目隐含的 &lt;strong&gt;单调性&lt;/strong&gt; 是优化性能的关键。这种性质通常意味着数据内部存在特定的偏序关系，或者最优决策点具有不可逆的移动趋势。为了高效地捕捉并利用这种结构化信息，我们通常需要借助单调数据结构。它们能够动态地维护一组具有单调特性的候选集合，通过实时剔除无效的冗余数据，帮助我们在大规模数据中精准锁定目标信息。&lt;/p&gt;
&lt;p&gt;这类数据结构的核心在于 &lt;strong&gt;决策剪枝&lt;/strong&gt; 。无论是寻找元素两侧最近极值的 &lt;strong&gt;单调栈&lt;/strong&gt; ，还是维护区间最值的 &lt;strong&gt;单调队列&lt;/strong&gt; ，亦或是动态调整边界的&lt;strong&gt;滑动窗口&lt;/strong&gt; ，其精髓都在于通过利用元素间的单调关系，将原本需要多次遍历的暴力检索优化为均摊常数级的快速响应。通过这种结构化的约束，复杂的区间操作被大幅压缩，使得算法在处理海量信息流时依然能够保持极高的运行效率。&lt;/p&gt;
&lt;h2&gt;单调滑动窗口&lt;/h2&gt;
&lt;p&gt;在各种数组与序列相关的题目中，滑动窗口几乎是最常见、也最实用的技巧之一。它的核心做法很简单：通过两个指针在序列上同步向前移动，始终维护一个当前的区间，并在移动的过程中不断检查区间是否满足题目的要求。不过，滑动窗口并不是可以随意使用的。它之所以高效，是因为许多问题本身具备一种关于窗口长度的单调性。典型的情形是：&lt;strong&gt;窗口越长越不满足条件，或者窗口越长越容易满足条件&lt;/strong&gt; 。只要存在这种单调性，我们在扩张右端点的时候，就能确保左端点不会回退，从而保证整个过程不会出现重复扫描的情况。&lt;/p&gt;
&lt;p&gt;其中一个典型的例子是「区间和不超过 $k$ 」。如果数组中的元素均为非负整数，那么随着窗口长度增加，窗口累加和只会上升而不会下降。这样一来，窗口越短就越满足条件，我们就可以放心地在累加和超过上限时收缩左端点，从而在线性时间内完成整个搜索。但如果数组中存在负整数，窗口累加和便不再单调，此时窗口累加和随着窗口的扩张反而会因负数的加入而下降。由于单调性被破坏，滑动窗口便失去原本的高效性，需要换用&lt;a href=&quot;#%E5%AF%BB%E6%89%BE%E7%B4%AF%E5%8A%A0%E5%92%8C%E8%87%B3%E5%A4%9A%E4%B8%BAk%E7%9A%84%E6%9C%80%E9%95%BF%E6%95%B0%E7%BB%84&quot;&gt;其他算法&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A31.png&quot; alt=&quot;滑动窗口图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于更复杂的滑动窗口极值问题，简单的双指针配合变量记录的方式已无法胜任。因为当旧的极值随左端点滑出窗口时，我们需要在 $O(1)$ 时间内找到新的极值，这时就需要配合 &lt;strong&gt;单调队列&lt;/strong&gt; 进行维护。单调队列通过在队尾剔除冗余数据，强制保证队列内部元素的单调性，从而保证队头始终指向当前窗口的局部极值。这种结构将原本需要 $O(k)$ 的区间检索优化到了均摊 $O(1)$ ，完美契合了滑动窗口对实时性的要求。&lt;/p&gt;
&lt;p&gt;对于滑动窗口中位数问题，情况则更具挑战性：由于中位数对数值的排序位置高度敏感，简单的单调性维护已不足以实现 $O(1)$ 的查询。这类问题通常需要借鉴&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/priority-queue/#%E6%95%B0%E6%8D%AE%E6%B5%81%E4%B8%AD%E4%BD%8D%E6%95%B0%E9%97%AE%E9%A2%98&quot;&gt;数据流中位数&lt;/a&gt;中经典的对顶堆架构。在窗口滑动过程中，我们不仅要动态维护两个堆的平衡以锁定窗口的中间值，还需额外引入延迟删除的机制来处理滑出窗口的过期元素。这种将双堆平衡与窗口移动相结合的策略，能将复杂度稳定在 $O(n \log k)$ ，是处理滑动窗口中位数问题的利器。&lt;/p&gt;
&lt;h2&gt;单调栈与队列&lt;/h2&gt;
&lt;p&gt;在处理序列相关问题时，我们不仅要关注数据的输入顺序，更需要 &lt;strong&gt;在扫描过程中持续维护&lt;/strong&gt; 某种局部最优或特定的约束关系。为了高效地记录并更新这些结构化信息，单调栈与单调队列成为了核心工具。它们的核心思路是 &lt;strong&gt;在容器内部强制维持单调性&lt;/strong&gt; ，通过预见性的筛选，只保留在后续状态中仍具备竞争力的有效元素，而果断剔除那些在当前逻辑下已失去价值的候选者。这种对冗余信息的实时清理机制，将复杂的区间关系巧妙地压缩进了线性处理流程中，从而实现了均摊 $O(1)$ 的极高处理效率。&lt;/p&gt;
&lt;p&gt;从结构上看，栈与队列同属线性容器，但操作维度的不同决定了它们的应用边界。栈严格遵循后进先出的逻辑，仅允许在单端进行存取；而双端队列则拥有更灵活的进出机制。这种接口能力的差异，使得它们在处理单调性问题时各司其职：&lt;strong&gt;单调栈&lt;/strong&gt; 更侧重于捕捉元素间的相互指向关系，如寻找左右两侧的最近极值；而 &lt;strong&gt;单调队列&lt;/strong&gt; 则与滑动窗口紧密结合，专注于维护动态区间内的实时最值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单调栈&lt;/strong&gt; 常用于解决 “下一个更大元素” 、“下一个更小元素” 等结构性问题。借助其单端更新的特性，我们可以在扫描序列时强制保持栈内的元素递增或递减。每当新元素尝试入栈却违反单调性时，栈顶那些比它更差的元素就会被永久弹出，因为对于后续的所有元素而言，当前的这个新元素不仅数值更优，而且位置更近，完全替代了旧元素的贡献。通过这种方式，我们可以在一次遍历中确定每个元素左右两侧最近的极值边界。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E5%8D%95%E8%B0%83%E6%A0%881.png&quot; alt=&quot;单调栈图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单调队列&lt;/strong&gt; 与滑动窗口的结合非常紧密，主要用于在窗口滑动的过程中实时维护区间的极值。不同于单调栈的单端操作，单调队列需要随着窗口的推进执行双重筛选：首先需要在队头弹出已经滑出窗口范围的过期元素；随后在队尾剔除那些数值不如新元素、且生存周期也更短的冗余元素。这种机制确保队列内部始终维持着一段严格单调的最优候选集合，使我们能够随时以 $O(1)$ 的代价，直接通过队头获取当前窗口内的全局最值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%971.png&quot; alt=&quot;单调队列图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;归根到底，无论使用单调栈还是单调队列，其目的都是一致的：&lt;strong&gt;利用数据之间的偏序关系与时序关系，主动压缩掉无效的候选集合，从而在一次遍历中完成本应需要多重循环才能实现的关系维护&lt;/strong&gt; 。它们不仅是简单的容器，更是一种动态过滤冗余信息的策略。通过将复杂的结构化扫描转化为简单的入队入栈操作，它们为涉及局部最值、区间判定以及动态规划状态转移优化等问题提供了极为高效且简洁的解决途径。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;普通滑动窗口模型&lt;/h1&gt;
&lt;p&gt;在处理数组或字符串的 &lt;strong&gt;子区间&lt;/strong&gt; 相关问题时，&lt;strong&gt;滑动窗口&lt;/strong&gt; 是一种兼具简洁与高效的经典算法技巧。其核心逻辑在于通过维护两个同向移动的指针，即左边界 $l$ 与右边界 $r$ ，在序列上动态地维护一个连续的窗口范围。通过这种双指针的协作，算法能够实时追踪区间内的属性变化，在不需要回退的前提下，精准定位符合约束条件的子区间，为后续的处理提供一个清晰且不断演进的观测窗口。&lt;/p&gt;
&lt;p&gt;该模型的应用精髓在于对问题 &lt;strong&gt;单调性&lt;/strong&gt; 的深度挖掘。在执行过程中，右指针 $r$ 主动向右推进以引入新元素，驱动窗口内的状态产生单向趋势变化。一旦窗口状态触及或突破预设的边界条件，如总和超过目标值 $k$ ，算法便由扩张转入收缩。通过左指针 $l$ 的右移来剔除陈旧元素，这种由左端的被动收缩来抵消右端扩张带来的状态偏移，本质上是在线性空间内寻找一种持续演进的 &lt;strong&gt;动态平衡&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这种机制巧妙利用了区间处理的连续性，通过 &lt;strong&gt;增量更新&lt;/strong&gt; 彻底告别了对重叠区域的重复计算。它将原本需要 $O(n^2)$ 暴力枚举的搜索空间大幅压缩至线性时间，在整个算法周期内，每个元素经历一进一出的完整过程，左右指针均遵循只进不退的原则，确保了每个数据点最多仅被访问两次。这种极致的执行效率使其成为处理大规模数据下区间约束问题的 &lt;strong&gt;首选方案&lt;/strong&gt; ，并在各种高性能数据过滤场景中展现出卓越的逻辑美感。&lt;/p&gt;
&lt;h2&gt;最长的休息间隔&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reschedule-meetings-for-maximum-free-time-i/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;一个公司有 $n$ 个会议，第 $i$ 个会议的开始时间为 &lt;code&gt;startTime[i]&lt;/code&gt; ，结束时间为 &lt;code&gt;endTime[i]&lt;/code&gt; 。所有的会议都在一天内进行，该天的总时长为 &lt;code&gt;eventTime&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;你可以通过移动会议来重新安排日程，但必须遵守以下规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;会议的 &lt;strong&gt;持续时间&lt;/strong&gt; 保持不变。&lt;/li&gt;
&lt;li&gt;会议之间的 &lt;strong&gt;相对顺序&lt;/strong&gt; 必须保持不变，且会议之间不能重叠。&lt;/li&gt;
&lt;li&gt;你最多可以移动 &lt;strong&gt;k&lt;/strong&gt; 个会议。&lt;/li&gt;
&lt;li&gt;移动后所有会议必须在 $[0, \text{eventTime}]$ 范围内。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你的目标是寻找一种移动方案，使得日程中出现一段 &lt;strong&gt;最长&lt;/strong&gt; 的连续空余时间。返回这段空余时间的最大长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq eventTime \leq 10^9$&lt;/li&gt;
&lt;li&gt;$n == startTime.length == endTime.length$&lt;/li&gt;
&lt;li&gt;$2 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n$&lt;/li&gt;
&lt;li&gt;$0 \leq startTime[i] &amp;lt; endTime[i] \leq eventTime$&lt;/li&gt;
&lt;li&gt;会议按 &lt;code&gt;startTime&lt;/code&gt; 升序排列且不重叠&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $n$ 、 $k$ 和 $eventTime$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示每个会议的开始时间 $startTime$ 。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示每个会议的结束时间 $endTime$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k \quad eventTime$&lt;/p&gt;
&lt;p&gt;$startTime_0 \quad startTime_1 \quad \ldots \quad startTime_{n-1}$&lt;/p&gt;
&lt;p&gt;$endTime_0 \quad endTime_1 \quad \ldots \quad endTime_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示重新安排后能获得的最大连续空余时间长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 1 5
1 3
2 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 1 10
0 2 9
1 4 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的核心在于 &lt;strong&gt;视角转换&lt;/strong&gt;：与其纠结于会议具体的起始与结束时间，不如将注意力转向会议之间的空隙。在 $n$ 个会议的序列中，天然存在着 $n+1$ 个间隔（包括首尾与边界的距离）。当我们拥有 $k$ 次移动会议的机会时，等同于我们可以撤走夹在某些间隔中间的 $k$ 个会议，从而将连续的 &lt;strong&gt;k + 1 个间隔&lt;/strong&gt; 强行汇聚成一段完整的空余时间。&lt;/p&gt;
&lt;p&gt;在具体实现上，我们可以将该问题转化为一个标准的 &lt;strong&gt;固定长度滑动窗口&lt;/strong&gt; 问题。我们预先提取出所有 $n+1$ 个间隔的长度并存入数组，随后利用大小为 $k+1$ 的窗口在数组上滑动。这种反向维护间隙而非正向维护会议的思路，极大地简化了题目中 “不改变相对顺序” 和 “不改变持续时间” 的复杂约束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
int n, k, eventTime;

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; k;
    cin &amp;gt;&amp;gt; eventTime;

    vector&amp;lt;int&amp;gt; startTime(n), endTime(n);
    for (int i = 0; i &amp;lt; n; i++) cin &amp;gt;&amp;gt; startTime[i];
    for (int i = 0; i &amp;lt; n; i++) cin &amp;gt;&amp;gt; endTime[i];

    vector&amp;lt;int&amp;gt; nums;
    for (int i = 0; i &amp;lt; (int) startTime.size(); i++){
        if (i == 0) nums.push_back(startTime[0] - 0);
        else nums.push_back(startTime[i] - endTime[i - 1]);
    }
    nums.push_back(eventTime - endTime[(int) endTime.size() - 1]);

    ll ans = 0, curSum = 0;
    if (k &amp;gt; (int) nums.size()) {
        for (int i = 0; i &amp;lt; (int) nums.size(); i++){
            ans += nums[i];
        }
        cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
    } else {
        for (int i = 0; i &amp;lt; k; i++){
            curSum += nums[i];
        }
        for (int i = k; i &amp;lt; (int) nums.size(); i++){
            curSum += nums[i];
            ans = max(ans, curSum);
            curSum -= nums[i - k];
        }
        cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;串联所有的单词&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/substring-with-concatenation-of-all-words/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个字符串 &lt;code&gt;s&lt;/code&gt; 和一个字符串数组 &lt;code&gt;words&lt;/code&gt; 。&lt;code&gt;words&lt;/code&gt; 中所有字符串的 &lt;strong&gt;长度相同&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;s&lt;/code&gt; 中的 &lt;strong&gt;串联子串&lt;/strong&gt; 是指包含 &lt;code&gt;words&lt;/code&gt; 中所有字符串以任意顺序排列连接而成的子串。&lt;/p&gt;
&lt;p&gt;返回所有串联子串在 &lt;code&gt;s&lt;/code&gt; 中的开始索引。你可以按 &lt;strong&gt;任意顺序&lt;/strong&gt; 返回答案。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq words.length \leq 5000$&lt;/li&gt;
&lt;li&gt;$1 \leq words[i].length \leq 30$&lt;/li&gt;
&lt;li&gt;$words[i]$ 和 $s$ 由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 $s$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $m$ ，表示 $words$ 数组的长度。&lt;/li&gt;
&lt;li&gt;第三行包含 $m$ 个字符串，由空格隔开，表示 $words$ 数组中的每个单词。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$m$&lt;/p&gt;
&lt;p&gt;$words_0 \quad words_1 \quad \ldots \quad words_{m-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一行整数，表示所有符合条件的起始索引，以空格隔开；如果不存在答案，请输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;barfoothefoobarman
2
foo bar
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wordgoodgoodgoodbestword
4
word good best word
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;barfoofoobarthefoobarman
3
foo bar the
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 9 12
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;本题最核心的转化在于将原字符串 &lt;code&gt;s&lt;/code&gt; 视为由若干个长度为 &lt;code&gt;len&lt;/code&gt; 的单词块构成的序列。在这种视角下，问题便由复杂的子串匹配降维成了经典的 &lt;strong&gt;计数滑动窗口&lt;/strong&gt; 问题。然而，由于单词的起始位置在原字符串中是连续的，单纯以 &lt;code&gt;len&lt;/code&gt; 为步长进行一次扫描会忽略掉那些不从索引 $0$ 开始的划分情况。为了确保扫描的完备性，我们需要依次以 $0, 1, 2, \dots, len-1$ 作为起点分别进行 &lt;strong&gt;偏移检测&lt;/strong&gt; 。这种多起点偏移的策略能够覆盖字符串中所有可能的单词切分方式，而 $len$ 之后的起点（如从 $len$ 开始）在逻辑上与其前面的起点完全对等，因此无需额外计算。&lt;/p&gt;
&lt;p&gt;在具体的执行流程中，每一组偏移扫描都可以看作是一个 &lt;strong&gt;独立的双指针滑动窗口&lt;/strong&gt; 过程。我们利用哈希表实时维护当前窗口内各单词的出现频次，右指针负责向右跳跃 &lt;code&gt;len&lt;/code&gt; 个字符加入新单词。当窗口内某个单词的频率超过了目标需求时，左指针便开始向右收缩，不断删除左侧单词直至窗口重新合法。由于我们严格保证了窗口内没有任何单词超标，只要当前窗口内的单词总数 &lt;code&gt;curm&lt;/code&gt; 恰好等于目标总数 &lt;code&gt;m&lt;/code&gt; ，就说明此时的窗口必然是由 &lt;code&gt;words&lt;/code&gt; 数组中所有单词的一种排列组合构成的，此时记录左边界对应的索引即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
int m; string s;

int main() {
    cin &amp;gt;&amp;gt; s; cin &amp;gt;&amp;gt; m;
    
    vector&amp;lt;string&amp;gt; words(m);
    unordered_map&amp;lt;string, int&amp;gt; cnts;
    for (int i = 0; i &amp;lt; m; i++) {
        cin &amp;gt;&amp;gt; words[i];
        cnts[words[i]]++;
    }

    int n = s.size();
    int len = words[0].size();
    vector&amp;lt;int&amp;gt; ans_indices;
    for (int i = 0; i &amp;lt; len; i++) {
        unordered_map&amp;lt;string, int&amp;gt; curCnt;
        int curm = 0; int left = i - len;
        for (int right = i; right + len &amp;lt;= n; right += len) {
            string str = s.substr(right, len);
            
            curCnt[str]++; curm++;
            while (curCnt[str] &amp;gt; cnts[str]) {
                left += len;
                string cur = s.substr(left, len);
                curCnt[cur]--;
                curm--;
            }

            if (curm == m) {
                ans_indices.push_back(left + len);
            }
        }
    }

    for (int i = 0; i &amp;lt; (int)ans_indices.size(); i++) {
        cout &amp;lt;&amp;lt; ans_indices[i] &amp;lt;&amp;lt; (i == (int)ans_indices.size() - 1 ? &quot;&quot; : &quot; &quot;);
    }
    cout &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使二进制字符串交替的最少反转&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-flips-to-make-the-binary-string-alternating/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个二进制字符串 &lt;code&gt;s&lt;/code&gt; 。你可以对字符串执行以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;删除&lt;/strong&gt;：删除字符串的第一个字符，并将其追加到字符串的末尾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反转&lt;/strong&gt;：选择字符串中的任一字符，将其从 &lt;code&gt;0&lt;/code&gt; 反转为 &lt;code&gt;1&lt;/code&gt; ，或者从 &lt;code&gt;1&lt;/code&gt; 反转为 &lt;code&gt;0&lt;/code&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目标是使字符串 &lt;code&gt;s&lt;/code&gt; 变为 &lt;strong&gt;交替字符串&lt;/strong&gt; ，求所需的 &lt;strong&gt;最少&lt;/strong&gt; 反转次数。&lt;/p&gt;
&lt;p&gt;交替字符串定义为：字符序列中没有相邻的字符相等。&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;&quot;01010&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;10101&quot;&lt;/code&gt; 是交替字符串，而 &lt;code&gt;&quot;0110&quot;&lt;/code&gt; 不是。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s[i]&lt;/code&gt; 为 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;1&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示使 &lt;code&gt;s&lt;/code&gt; 变为交替字符串所需的最少反转次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;111000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;010
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1110
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;操作 $1$ 的 &lt;strong&gt;本质就是轮换&lt;/strong&gt; ，而处理轮换的常见方式是 &lt;strong&gt;将原数组倍增后滑窗&lt;/strong&gt; 。当我们维持一个长度固定为 $n$ 的窗口在倍增数组上向右滑动时，窗口每滑动一次就等价于原数组进行一次轮换操作。通过这种方式，我们可以消除模拟轮换带来的高额时间复杂度。&lt;/p&gt;
&lt;p&gt;对于交替字符串，其最终的目标形态本质上只有两种。一种是形如 &lt;code&gt;101010&lt;/code&gt; 的字符串，另一种是形如 &lt;code&gt;010101&lt;/code&gt; 的字符串。为了避免在滑动窗口的过程中对这两种目标形态分别进行复杂的分类讨论，我们可以巧妙地引入一个 &lt;strong&gt;全局参考基准线&lt;/strong&gt; ，并采用 “错位映射” 的数学思想将它们统一起来。&lt;/p&gt;
&lt;p&gt;我们不妨以第二种形式作为全局参考基准线，这意味着在目标基准线上，所有偶数下标对应的字符应当为 &lt;code&gt;&apos;0&apos;&lt;/code&gt; ，所有奇数下标对应的字符应当为 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 。此时，我们将倍增字符串中的每一个字符与该基准线进行比对。如果当前字符不满足这个奇偶交替的规律，我们就将其视为一个错位点，也就是需要进行类型 $2$ 反转操作的位置。&lt;/p&gt;
&lt;p&gt;随着窗口在倍增字符串上向右滑动，具体的错位映射逻辑会出现以下两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一种情况，当子串左端点下标是偶数时。因为左端点在全局基准线上本来就是偶数位置，所以此时窗口内部的奇偶性与全局基准线 &lt;strong&gt;完全对齐&lt;/strong&gt; 。这意味着如果我们想把这个子串变成 &lt;code&gt;010101&lt;/code&gt; 形态，那些 &lt;strong&gt;不符合&lt;/strong&gt; 全局基准线的字符就需要反转；如果我们想把它变成 &lt;code&gt;101010&lt;/code&gt; 形态，那些 &lt;strong&gt;符合&lt;/strong&gt; 全局基准线的字符就需要反转。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二种情况，当子串左端点下标是奇数时。因为左端点在全局基准线上变成奇数位置，这导致整个窗口的奇偶性相对于全局基准线 &lt;strong&gt;整体向右偏移 1 位&lt;/strong&gt; 。这意味着如果我们想把子串变成 &lt;code&gt;010101&lt;/code&gt; 形态，那些 &lt;strong&gt;符合&lt;/strong&gt; 全局基准线的字符就需要反转；如果我们想把它变成 &lt;code&gt;101010&lt;/code&gt; 形态，那些 &lt;strong&gt;不符合&lt;/strong&gt; 全局基准线的字符就需要反转。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这两种情况中，由于窗口内的总字符数固定为 $n$ ，如果我们引入一个计数器 $cnt$ ，专门用来动态统计当前窗口内 &lt;strong&gt;不符合&lt;/strong&gt; 全局基准线的错位点个数，那么当前窗口内 &lt;strong&gt;符合&lt;/strong&gt; 全局基准线的字符个数自然就为 $n - cnt$ 。此时我们可以清晰地看到，无论子串的左端点下标是奇数还是偶数，其最小反转次数均可以通过以下公式统一表达：&lt;/p&gt;
&lt;p&gt;$$
Ans = \min(cnt, n - cnt)
$$&lt;/p&gt;
&lt;p&gt;利用这个核心公式，复杂的奇偶状态与目标分类被彻底消解。在接下来的滑动窗口过程中，我们只需要动态维护这个计数器 $cnt$ ，并不断用该公式的计算结果去更新全局最优解即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;获得最多的硬币&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-coins-from-k-consecutive-bags/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在一条数轴上有无限多个袋子，每个坐标对应一个袋子。其中一些袋子里装有硬币。&lt;/p&gt;
&lt;p&gt;给你一个二维数组 &lt;code&gt;coins&lt;/code&gt; ，其中 &lt;code&gt;coins[i] = [li, ri, ci]&lt;/code&gt; 表示从坐标 &lt;code&gt;li&lt;/code&gt; 到 &lt;code&gt;ri&lt;/code&gt; 的每个袋子中都有 &lt;code&gt;ci&lt;/code&gt; 枚硬币。这些区间是互不重叠的。&lt;/p&gt;
&lt;p&gt;另给你一个整数 &lt;code&gt;k&lt;/code&gt; 。返回通过收集连续 &lt;code&gt;k&lt;/code&gt; 个袋子可以获得的 &lt;strong&gt;最多&lt;/strong&gt; 硬币数量。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq coins.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq li \leq ri \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq ci \leq 1000$&lt;/li&gt;
&lt;li&gt;给定的区间互不重叠&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 行，每行包含三个整数，表示第 $i$ 个区间的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$li_1 \quad ri_1 \quad ci_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$li_n \quad ri_n \quad ci_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示收集连续 $k$ 个袋子可获得的最大硬币数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 4
8 10 1
1 3 2
5 6 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2
1 10 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;使数组连续难题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-operations-to-make-array-continuous/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 。每一次操作中，你可以将数组中任一元素替换为 &lt;strong&gt;任意整数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果数组满足以下条件，则称其为 &lt;strong&gt;连续&lt;/strong&gt; 的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数组中所有元素都是 &lt;strong&gt;唯一&lt;/strong&gt; 的（没有重复元素）。&lt;/li&gt;
&lt;li&gt;数组中最大元素与最小元素之间的差值等于 &lt;code&gt;nums.length - 1&lt;/code&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，&lt;code&gt;nums = [4, 2, 5, 3]&lt;/code&gt; 是连续的，因为重新排序后得到 &lt;code&gt;[2, 3, 4, 5]&lt;/code&gt; ，最大值与最小值差为 $5 - 2 = 3$ ，且长度为 $4$ 。而 &lt;code&gt;nums = [1, 2, 3, 5, 6]&lt;/code&gt; 不是连续的。&lt;/p&gt;
&lt;p&gt;请返回使 &lt;code&gt;nums&lt;/code&gt; 成为连续数组所需的 &lt;strong&gt;最少&lt;/strong&gt; 操作次数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示使数组连续所需的最少操作次数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
4 2 5 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 2 3 5 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
1 10 100 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;正难则反，维护不需要改变的部分&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;种类滑动窗口问题&lt;/h1&gt;
&lt;p&gt;在处理字符串序列问题时，&lt;strong&gt;种类滑动窗口&lt;/strong&gt; 是一种通过人为施加约束来构造单调性的精妙技巧。标准的滑动窗口通常要求窗口内的总长度具备某种天然的单调性，但在处理诸如计算包含相同频率字符的最长子串等问题时，窗口往往并不满足越长越好的简单单调逻辑，这使得常规的双指针方案难以直接奏效。为了破解这一困局，我们通常采取 &lt;strong&gt;枚举字符种类数&lt;/strong&gt; 的核心策略，将复杂的全局问题拆解为在窗口内恰好包含 $k$ 种字符时的局部最优解，从而为滑动窗口创造了先决条件。&lt;/p&gt;
&lt;p&gt;这种方法的核心在于利用了字符集规模通常较小的客观事实。以仅包含小写字母的字符串为例，其字符集规模 $\Sigma$ 的最大值仅为 $26$ 。我们通过一个外层循环固定当前的种类数限制 $target$ ，随后在内层执行标准的滑动窗口逻辑。右指针不断向右扩展并维护一个频率数组，一旦当前窗口内的不同字符总数超过了 $target$ ，左指针便开始收缩。这种强行制造单调性的做法，将原本复杂的组合搜索转化为了 $\Sigma$ 次标准的线性窗口扫描。虽然从形式上看复杂度增加了 $\Sigma$ 倍，但在 $O(\Sigma \cdot N)$ 的量级下，它在处理长字符串题目时展现出了极高的运行效率。&lt;/p&gt;
&lt;p&gt;通过引入字符种类数这一额外的逻辑维度，我们将一个看似不具备单调性的全局搜索问题，转化为了若干个局部单调的子问题。在每一次子问题的求解过程中，由于种类数被严格限制，窗口的收缩逻辑变得清晰且唯一，从而确保了双指针移动的有效性与正确性。&lt;/p&gt;
&lt;h2&gt;统计完全字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-complete-substrings/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;word&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果一个字符串满足以下条件，则称它是一个 &lt;strong&gt;完全子字符串&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;字符串中的每个字符都恰好出现 &lt;code&gt;k&lt;/code&gt; 次。&lt;/li&gt;
&lt;li&gt;相邻字符在字母表中的顺序至多相差 $1$（即 &lt;code&gt;abs(word[i] - word[i+1]) &amp;lt;= 1&lt;/code&gt; ）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;返回 &lt;code&gt;word&lt;/code&gt; 中完全子字符串的数目。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq word.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq word.length$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;word&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 $word$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $k$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$word$&lt;/p&gt;
&lt;p&gt;$k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示 &lt;code&gt;word&lt;/code&gt; 中完全子字符串的数目。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;igigee
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaabbbccc
3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;K重复最长子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;s&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，请你找出 &lt;code&gt;s&lt;/code&gt; 中的最长子串，要求该子串中的每一字符出现次数都不少于 &lt;code&gt;k&lt;/code&gt; 。如果有多个这样的子串，返回其中最长的长度。&lt;/p&gt;
&lt;p&gt;如果不存在这样的子字符串，则返回 $0$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 10^4$&lt;/li&gt;
&lt;li&gt;$s$ 仅由小写英文字母组成&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个字符串 $s$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个整数 $k$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;p&gt;$k$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示符合要求的最长子串的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaabb
3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ababbc
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;分组滑动窗口问题&lt;/h1&gt;
&lt;p&gt;在处理复杂的序列约束时，&lt;strong&gt;分组滑动窗口&lt;/strong&gt; 是一种通过 &lt;strong&gt;数据解耦&lt;/strong&gt; 来简化逻辑的高阶策略。在许多数组或字符串题目中，我们需要处理某种特定元素（如数字 $x$ 或字符 $c$ ）的连续段性质，而原数组中往往存在大量无关元素的交替干扰，导致直接维护全局窗口时逻辑极其冗余。该方法的核心思想在于 &lt;strong&gt;按值归类与化繁为简&lt;/strong&gt; ，即预先利用动态数组将每种元素出现的所有下标分别提取出来，从而将杂乱无章的原始数组拆解为若干个独立的下标序列。&lt;/p&gt;
&lt;p&gt;在这种独立序列上进行滑动窗口，本质上是在研究该元素第 $i$ 次出现与第 $j$ 次出现之间遮蔽了多少其他元素，或者其物理距离是否满足某种特定的约束。这种模型将混乱的全局状态转化为了 &lt;strong&gt;局部有序的窗口移动&lt;/strong&gt; ，使得我们能够极速计算出特定元素在满足间隙限制下的最长连续段。由于窗口操作仅在同类元素的下标集合内进行，原本复杂的区间判定被抽象成对下标差值的线性扫描，极大地提升了算法的直观性与执行效率。&lt;/p&gt;
&lt;p&gt;该模型在处理涉及 &lt;strong&gt;修改操作&lt;/strong&gt; 的题目时表现尤为出色，其精髓在于每种元素的下标序列在逻辑上是相对独立的。这种局部的思维方式有效避免了对无关数据的无效扫描，通过将原本复杂的全局计数转化为局部下标间的运算，使得原本难以处理的修改问题变得非常直观。这种从原始序列中剥离核心数据的做法，不仅精简了算法的流程，更让整体逻辑变得清晰且易于维护。&lt;/p&gt;
&lt;h2&gt;最长等值子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-the-longest-equal-subarray/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个下标从 $0$ 开始的整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果子数组中所有元素都相等，则认为子数组是 &lt;strong&gt;等值&lt;/strong&gt; 的。注意，空子数组是等值的。&lt;/p&gt;
&lt;p&gt;在从数组中删除最多 &lt;code&gt;k&lt;/code&gt; 个元素后，返回其中最长的等值子数组的长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] \leq nums.length$&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq nums.length$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，分别表示数组长度和最多可删除的元素个数。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \dots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示删除最多 $k$ 个元素后能得到的最长等值子数组的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 3
1 3 2 3 1 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 2
1 1 2 2 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;单字符重复子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-for-longest-repeated-character-substring/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;如果字符串中的所有字符都相同，那么这个字符串是单字符重复的字符串。&lt;/p&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;text&lt;/code&gt; ，你只能交换其中两个字符一次或者什么都不做，然后得到一些单字符重复的子串。返回其中最长的子串的长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq text.length \leq 2 \cdot 10^4$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text&lt;/code&gt; 仅由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入仅包含一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$text$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在执行最多一次交换后，单字符重复子串的最大长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ababa
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaabaaa
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aaabbaaa
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;带修最长字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-repeating-character-replacement/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;s&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; 。你可以选择字符串中的任一字符，并将其更改为任何其他大写英文字符。该操作最多可执行 &lt;code&gt;k&lt;/code&gt; 次。&lt;/p&gt;
&lt;p&gt;在执行上述操作后，返回包含相同字母的最长子串的长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq s.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由大写英文字母组成&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq s.length$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含一个字符串 $s$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$k$&lt;/p&gt;
&lt;p&gt;$s$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示在最多替换 $k$ 次字符后，包含相同字母的最长子串的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
ABAB
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
AABABBA
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;最近上与下邻问题&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最近上/下邻问题&lt;/strong&gt; 是单调栈最经典的应用场景。它通过维护一个特定的单调序列，将原本需要 $O(N^2)$ 的暴力检索优化为线性时间的实时响应。在遍历过程中，算法利用栈后进先出的特性，动态剔除那些在后续比较中 &lt;strong&gt;已经失去竞争力的冗余元素&lt;/strong&gt; ，从而为每个元素准确定位其两侧的最近邻。这种剪枝机制确保了每个元素在整个生命周期中仅进出栈一次，实现了整体 $O(N)$ 的高效处理，为解决复杂的区间边界问题提供了清晰的逻辑起点。&lt;/p&gt;
&lt;p&gt;以寻找 &lt;strong&gt;两侧最近上邻&lt;/strong&gt;（即左侧和右侧第一个比当前元素大的数）为例，问题的本质在于识别哪些元素能成为候选答案。当我们从左向右扫描时，如果当前元素比前面出现的某些元素都要大，那么它在逻辑上就形成了对这些旧元素的 &lt;strong&gt;绝对支配&lt;/strong&gt; 。其原因非常直观：对于后续待处理的元素而言，当前这个新元素不仅数值更优，而且在位置上也更靠近自身。在这种双重优势下，那些既小又远的旧元素便彻底失去了作为最近上邻的竞争力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%91%E4%B8%8A%E9%82%BB1.png&quot; alt=&quot;最近上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果当前元素并不是目前的最大值，情况则演变为一种 &lt;strong&gt;梯队式的筛选&lt;/strong&gt; 。根据上述支配逻辑，当前元素前面所有比它小的元素都已经失效，应当被果断剔除。然而，那些原本就比当前元素更大的旧元素依然具有保留价值，因为虽然当前元素位置更靠后，但它的数值大小不足以完全替代前面那些数值更大的元素。这种筛选机制确保了我们的候选集合始终处于一种 &lt;strong&gt;优胜劣汰&lt;/strong&gt; 的动态平衡中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%91%E4%B8%8A%E9%82%BB2.png&quot; alt=&quot;最近上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由此我们可以提炼出一个核心规则：&lt;strong&gt;每当新元素入场，就应当将其与栈顶元素进行比较，将候选集合中所有被其支配（即数值更小）的元素强制弹出&lt;/strong&gt;。在不断剔除这些冗余项后，剩余的候选元素在数值上必然是 &lt;strong&gt;单调递减&lt;/strong&gt; 的序列，这在逻辑结构上正好契合了单调栈的维护逻辑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%91%E4%B8%8A%E9%82%BB3.png&quot; alt=&quot;最近上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基于这一结构，问题求解过程就会变得极其高效：每当新元素准备入栈时，在完成剔除操作后的 &lt;strong&gt;当前栈顶元素&lt;/strong&gt; ，恰好就是它左侧扫描路径上的首个高位，即该元素左侧的最近上邻。同理，如果我们采用相同的逻辑反向遍历序列，便可以镜像地求出每个元素在右侧的最近上邻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%91%E4%B8%8A%E9%82%BB4.png&quot; alt=&quot;最近上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然而更精妙的是，我们并不需要显式地进行反向遍历，只需沿着单调栈的思路进一步分析：当栈中存在比当前元素更小的值时，它们必然会在当前元素入栈时被弹出；而栈中的任意一个元素，只要在后续过程中首次遇到一个比自己更大的元素，就一定会在那一刻出栈。因此 &lt;strong&gt;元素被弹出的瞬间，恰好意味着它第一次遇到了右侧比它大的元素&lt;/strong&gt; ，这正是最近上邻的定义。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%91%E4%B8%8A%E9%82%BB5.png&quot; alt=&quot;最近上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种 &lt;strong&gt;以空间换时间&lt;/strong&gt; 的策略不仅规避了低效的重复扫描，更直观地反映了序列元素间的大小与位置约束。通过将复杂的结构化查找转化为简单的入栈与出栈操作，单调栈为解决直方图最大矩形、接雨水问题以及各类复杂的区间贡献统计提供了最为简洁、高效的底层逻辑，实现了从暴力搜索到优雅剪枝的质变。&lt;/p&gt;
&lt;h2&gt;寻找累加和至少为K的最短数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 $k$ ，找出 &lt;code&gt;nums&lt;/code&gt; 中和至少为 $k$ 的 &lt;strong&gt;最短非空子数组&lt;/strong&gt; ，并返回该子数组的长度。如果不存在这样的 &lt;strong&gt;子数组&lt;/strong&gt; ，返回 $-1$ 。&lt;/p&gt;
&lt;p&gt;子数组是数组中 &lt;strong&gt;连续&lt;/strong&gt; 的一部分。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-10^5 \leq nums[i] \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 1
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 4
1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题目的核心目标是在含有负数的数组中，寻找和至少为 $k$ 的最短连续子数组。处理连续子数组和的问题，最自然的切入点是引入前缀和数组。令 $pre[i]$ 表示原数组前 $i$ 个元素的和，那么任意一个连续子数组 &lt;code&gt;nums[l...r-1]&lt;/code&gt; 的和就可以转化为两个前缀和的差值，即 $pre[r] - pre[l]$ 。于是，题目要求的核心数学不等式可以写为 $pre[r] - pre[l] \geq k$ 。&lt;/p&gt;
&lt;p&gt;为了方便通过遍历寻找最优解，我们可以将该不等式进行移项变形，得到以下形式：&lt;/p&gt;
&lt;p&gt;$$
pre[l] \leq pre[r] - k
$$&lt;/p&gt;
&lt;p&gt;该公式表明，当固定右端点 $r$ 时，我们需要在它的左边寻找一个满足条件的左端点 $l$ ，使得 $l$ 对应的前缀和数值小于等于 $pre[r] - k$ 。在所有满足该条件的左端点中，为了让子数组的长度 $r - l$ 尽量小，我们应该让 $l$ 尽量靠右，这本质上是一个最近下邻问题的变形。&lt;/p&gt;
&lt;p&gt;由于原数组中存在负数，前缀和数组 $pre$ 会呈现出上下波动的非单调特性，这导致传统的双指针滑动窗口算法在此处失效。为了在非单调的前缀和序列中高效筛选出最优的左端点，我们需要借助单调双端队列来动态维护前缀和的下标，并保持队列中对应的前缀和数值严格单调递增。&lt;/p&gt;
&lt;p&gt;随着右端点 $r$ 的向右移动，队列的维护逻辑主要分为两步。第一步是自队头向后查找可行解，若队头元素满足 $pre[r] - pre[dq.front()] \geq k$ ，则该队头已完成其作为左端点的最短历史使命，我们在更新全局最小长度后将其弹出。第二步是自队尾向前维护单调性，若当前 $pre[r]$ 小于等于队尾元素对应的前缀和，由于 $r$ 位置更靠右且数值更小，旧的队尾已被完全替代，我们将其从队尾弹出。&lt;/p&gt;
&lt;p&gt;在这套双向剔除的机制下，每个元素的下标在整个遍历过程中最多只会入队一次和出队一次。这使得我们可以彻底告别暴力枚举的高额复杂度，将整个寻找最短区间的时间复杂度完美控制在线性级别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组最大幸运值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF280B&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $n$ 的整数数组 $a$ 。你需要找出数组中任意两个元素 $a[i]$ 和 $a[j]$（ $i \le j$ ），使得它们的异或和 $a[i] \oplus a[j]$ 最大。该异或和必须满足：对于所有满足 $i &amp;lt; k &amp;lt; j$ 的 $k$ ，都有 $a[k] &amp;lt; \min(a[i], a[j])$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的最大异或和。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
5 2 1 4 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;枚举次大值，用单调栈求解可能的最大值&lt;/p&gt;
&lt;h2&gt;变化阈值子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subarray-with-elements-greater-than-varying-threshold/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;threshold&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;找出数组 &lt;code&gt;nums&lt;/code&gt; 中的一个子数组，且满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子数组的长度为 &lt;code&gt;k&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;子数组中的每个元素都大于 &lt;code&gt;threshold / k&lt;/code&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;返回满足条件的任意子数组的 &lt;strong&gt;长度&lt;/strong&gt; &lt;code&gt;k&lt;/code&gt;。如果没有这样的子数组，返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i], threshold \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $threshold$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad threshold$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的子数组长度 $k$ ；若不存在，输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 6
1 3 4 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 7
6 5 6 5 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;hr /&gt;
&lt;h1&gt;最远上与下邻问题&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最远上/下邻问题&lt;/strong&gt; 与最近邻问题的核心差异在于对单调性的利用逻辑。最近邻关注的是生存竞争下的局部排除，而最远邻则侧重于历史存留中的全局跨度。该问题的挑战在于如何在长序列中快速定位最远的目标，通常需要通过 &lt;strong&gt;预处理单调序列&lt;/strong&gt; 配合 &lt;strong&gt;二分查找&lt;/strong&gt; ，将暴力检索的 $O(N^2)$ 复杂度优化至 $O(N \log N)$ 。这种策略利用了单调性提供的有序检索空间，在确保不漏掉任何潜在解的同时，极大地提升了在大规模数据下锁定最远边界的能力。&lt;/p&gt;
&lt;p&gt;以寻找 &lt;strong&gt;两侧最远上邻&lt;/strong&gt;（即位于最左侧和最右侧且比当前元素大的数）为例，问题的核心在于识别哪些历史元素具备成为最远目标的潜力。由于我们追求的是相对最远的位置，那么位置越靠前的元素，其价值自然就越高。基于这一观察，如果一个较晚出现的元素，其数值甚至还不如它左侧已有的某个元素大，那么它在位置和数值上都处于劣势，便永远不可能成为后续任何元素的最远上邻。因此，真正有资格进入候选集合的，必然是那些 &lt;strong&gt;刷新了历史最值的元素&lt;/strong&gt; ，这些元素必然会在数值上呈现出严格的 &lt;strong&gt;单调递增&lt;/strong&gt; 形式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%9C%E4%B8%8A%E9%82%BB1.png&quot; alt=&quot;最远上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当新元素尝试与这个候选集合匹配时，处理逻辑从原本的 &lt;strong&gt;末端剔除&lt;/strong&gt; 转向了 &lt;strong&gt;内部检索&lt;/strong&gt; 。如果当前值小于栈顶元素，说明其左侧确实存在合法的上邻。但为了追求最远的跨度，仅仅找到一个大于当前值的数是不够的，我们必须在所有比它大的候选中，精准锁定那个 &lt;strong&gt;位置最靠前的元素&lt;/strong&gt; 。本质上，这相当于在有序的单调序列中执行一次 &lt;strong&gt;二分查找&lt;/strong&gt;，可以在 $O(\log N)$ 的时间内精准锁定符合要求的元素。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%9C%E4%B8%8A%E9%82%BB2.png&quot; alt=&quot;最远上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;strong&gt;最远邻问题无法像最近邻问题那样，仅通过单次遍历便同时结算两侧的答案&lt;/strong&gt; 。这是由最远邻判定对 &lt;strong&gt;全局位置极值&lt;/strong&gt; 的高度依赖性决定的。在单向扫描过程中，算法无法即时判定当前匹配的合法元素是否为物理意义上的最远边界，因为序列未遍历的远端仍可能存在更优解。这种 &lt;strong&gt;信息的滞后性&lt;/strong&gt; 决定了该问题必须通过 &lt;strong&gt;正向遍历求左侧最远邻&lt;/strong&gt; 与 &lt;strong&gt;反向遍历求右侧最远邻&lt;/strong&gt; 两个独立的流程来完成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmonotonic-structure%5C%E6%9C%80%E8%BF%9C%E4%B8%8A%E9%82%BB3.png&quot; alt=&quot;最远上邻图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从更宏观的视角来看，单调栈在处理此类问题时表现出一种只进不出的单向累加状态，这其实揭示了它与前缀最大值结构的等价性。为了进一步简化代码实现，我们可以预处理一个前缀最大值数组：&lt;/p&gt;
&lt;p&gt;$$
mx[i] = \max(a[1], a[2], \ldots, a[i])
$$&lt;/p&gt;
&lt;p&gt;由于该数组本身具有天然的单调不降属性，对于当前位置 $i$ ，如果 $mx[i-1] \leq a[i]$ ，那么显然左侧不存在更大的元素；否则，说明在区间 $[1, i-1]$ 内一定存在满足条件的解。此时，由于前缀最大值具有单调性，我们可以在前缀最大值数组上通过二分查找，找到 &lt;strong&gt;最早使得前缀最大值大于当前值的位置&lt;/strong&gt; ，从而确定最远上邻。&lt;/p&gt;
&lt;h2&gt;寻找累加和至多为K的最长数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/3473e545d6924077a4f7cbc850408ade&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个无序整数数组 &lt;code&gt;arr&lt;/code&gt; ，其中元素可以为正、负或 $0$ ；同时给定一个整数 &lt;code&gt;k&lt;/code&gt; 。
请你在数组中找到 &lt;strong&gt;所有累加和 ≤ k 的子数组&lt;/strong&gt; 中，长度 &lt;strong&gt;最长&lt;/strong&gt; 的子数组的长度并输出该长度。&lt;/p&gt;
&lt;p&gt;子数组必须是连续的一段，不可以跳跃选取。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq k \leq 10^9$&lt;/li&gt;
&lt;li&gt;$-100 \leq arr_i \leq 100$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$arr_0 \quad arr_1 \quad \ldots \quad arr_{N-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示累加和小于或等于 $k$ 的最长子数组的长度。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 -2
3 -2 -4 0 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题可以转化为最远上邻问题。首先我们构造前缀和数组 &lt;code&gt;pre[i]&lt;/code&gt; ，使得任意子数组 $[l, r]$ 的和可以表示为：&lt;/p&gt;
&lt;p&gt;$$
sum(l, r) = pre[r+1] - pre[l]
$$&lt;/p&gt;
&lt;p&gt;题目要求子数组和不超过 $k$ ，即：&lt;/p&gt;
&lt;p&gt;$$
pre[r+1] - pre[l] \leq k \quad \Rightarrow \quad pre[r+1] \leq k + pre[l]
$$&lt;/p&gt;
&lt;p&gt;通过这个变换我们可以将原问题转化为最远上邻问题：对每个右端点 $r$ ，我们希望找到最左的 $l$ ，满足 &lt;code&gt;pre[l]&lt;/code&gt; 足够大，使得右端点对应的子数组和不超过 $k$ 。也就是说，右端点对应的最长子数组长度就等于 $r - l + 1$ ，而最左上邻的位置正是 $l$ 。&lt;/p&gt;
&lt;p&gt;为了快速查找最左上邻，我们可以维护前缀最小值数组 &lt;code&gt;min_pre&lt;/code&gt; ，记录 &lt;code&gt;pre&lt;/code&gt; 数组的历史最小值。对于每个右端点，通过二分查找 &lt;code&gt;min_pre&lt;/code&gt; 中第一个满足 &lt;code&gt;pre[l] &amp;gt;= pre[r+1] - k&lt;/code&gt; 的位置，就能得到最左上邻。这种做法完全对应最远上邻模板：历史候选元素构成单调集合，右端点查询最远满足条件的左端点，然后更新答案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这道题可以进一步优化为更快的滑动窗口解法，但由于 &lt;strong&gt;数组中可能存在负数&lt;/strong&gt; ，我们不能直接使用普通的滑动窗口。为了能正常处理这种情况，我们可以先用 DP 预处理每个位置往右延伸的 &lt;strong&gt;最小累加和子数组&lt;/strong&gt; 。具体来说，这个解法需要定义两个 DP 数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minSum[i]&lt;/code&gt;：表示从位置 $i$ 开始往右延伸的子数组中，累加和最小的那个子数组的和。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minSumEnd[i]&lt;/code&gt;：表示对应 &lt;code&gt;minSum[i]&lt;/code&gt; 的子数组终点位置，即最小累加和子数组的右端位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这两个数组之后，我们可以在滑动窗口中快速寻找最长子数组。右端点 $r$ 利用 DP 信息直接跳跃：当窗口扩展到某个右端点时，查 &lt;code&gt;minSum[r]&lt;/code&gt; 得到从 $r$ 开始的最小累加和子数组，如果整个子数组加上当前窗口和不超过 $k$ ，则右端点可以直接跳到 &lt;code&gt;minSumEnd[r] + 1&lt;/code&gt; ，一次性覆盖整个最小累加和子数组。如果窗口无法容纳该最小累加和，则说明从当前左端点开始延伸的子数组已经无法满足条件，此时左端点向右移动一格进行缩窗。&lt;/p&gt;
&lt;p&gt;通过这种方式，左端点每次只移动一格，而右端点通过最小累加和子数组进行跳跃扫描，每个元素最多被访问一次，因此整个算法的时间复杂度为 $O(N)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;滑动窗口极值问题&lt;/h1&gt;
&lt;p&gt;在算法竞赛中，&lt;strong&gt;滑动窗口极值问题&lt;/strong&gt; 是一类极其经典且高频的题型。其核心任务是给定一个长度不固定的窗口，要求在窗口沿数组从左向右滑动的过程中，实时获取每个窗口内元素的最值。如果采用暴力手段对每个窗口重新进行区间扫描，整体时间复杂度将达到 $O(nk)$ ，这在面对大规模数据时往往会超出时间限制。&lt;/p&gt;
&lt;p&gt;解决这类问题的核心突破点在于识别窗口移动过程中的 &lt;strong&gt;连续性与重叠性&lt;/strong&gt; 。由于每次滑动仅涉及一个旧元素的移出与一个新元素的引入，相邻窗口之间绝大多数元素是相同的。基于这一特征，我们并不需要关注窗口内的所有成员，而应聚焦于那些 &lt;strong&gt;仍具竞争力的候选元素&lt;/strong&gt; ，即那些在当前或未来的滑动周期内，依然有可能成为极值的元素。&lt;/p&gt;
&lt;p&gt;以维护区间最大值为例，当一个新元素进入窗口时，如果它大于窗口内已有的某些元素，那么这些数值较小且位置靠左的旧元素便失去了意义。即便它们仍在窗口的物理范围内，但因为新元素不仅数值更大，而且在时间维度上存活得更久，旧元素已经不可能再成为后续任何窗口的最大值。这种现象被称为 &lt;strong&gt;支配关系&lt;/strong&gt; 。通过在处理时主动剔除这些失去竞争力的冗余数据，并让剩下的元素在队列中按数值维持严格的单调递减，我们便构造出了 &lt;strong&gt;单调队列&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在单调队列的结构下，队头元素始终对应当前窗口的全局极值，从而实现了 &lt;strong&gt;均摊 $O(1)$ 复杂度的获取效率&lt;/strong&gt; 。随着窗口左端点的向右移动，算法采用 &lt;strong&gt;延迟弹出机制&lt;/strong&gt; 确保数据的时效性：即在查询最值前，持续校验队头元素的索引，一旦判定其已越过当前窗口左边界，便将其从队头移除。尽管在某些步骤中可能伴随多次弹出操作，但由于每个元素在整个流程中 &lt;strong&gt;至多入队一次、出队一次&lt;/strong&gt; ，整体时间复杂度被稳定在 $O(n)$ 的线性级别。&lt;/p&gt;
&lt;h2&gt;接雨水最小花盆&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2698&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定 $n$ 个雨滴的坐标 $(x_i, y_i)$ ，你需要选择一个宽度为 $w$ 的花盆（即花盆覆盖的水平区间为 $[x, x+w]$ ），使得花盆内所有雨滴的纵坐标极差（最大纵坐标与最小纵坐标之差）至少为 $d$ 。&lt;/p&gt;
&lt;p&gt;求满足条件的最小花盆宽度 $w$ 。如果不存在这样的宽度，返回 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq d \leq 10^6$&lt;/li&gt;
&lt;li&gt;$0 \leq x_i, y_i \leq 10^6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $d$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含两个整数 $x_i$ 和 $y_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad d$&lt;/p&gt;
&lt;p&gt;$x_1 \quad y_1$&lt;/p&gt;
&lt;p&gt;$x_2 \quad y_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$x_n \quad y_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的最小花盆宽度 $w$ 。若不存在，输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 5
6 3
2 4
4 10
12 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;不等条件最大值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/max-value-of-equation/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个数组 &lt;code&gt;points&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; 。数组中每个元素都表示二维平面上的点的坐标，其中 &lt;code&gt;points[i] = [xi, yi]&lt;/code&gt; ，并且按照 &lt;code&gt;xi&lt;/code&gt; 从小到大排序。&lt;/p&gt;
&lt;p&gt;请你返回 &lt;code&gt;yi + yj + |xi - xj|&lt;/code&gt; 的最大值，其中 &lt;code&gt;|xi - xj| &amp;lt;= k&lt;/code&gt; 且 &lt;code&gt;1 &amp;lt;= i &amp;lt; j &amp;lt;= points.length&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq points.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$points[i].length == 2$&lt;/li&gt;
&lt;li&gt;$-10^8 \leq points[i][0], points[i][1] \leq 10^8$&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq 2 \cdot 10^8$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;points&lt;/code&gt; 中的所有点坐标 $xi$ 互不相同，且按 $xi$ 升序排列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ ，分别表示数组长度和最大水平距离限制。&lt;/li&gt;
&lt;li&gt;接下来的 $n$ 行，每行包含两个整数 $xi$ 和 $yi$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$x_1 \quad y_1$&lt;/p&gt;
&lt;p&gt;$x_2 \quad y_2$&lt;/p&gt;
&lt;p&gt;$\dots$&lt;/p&gt;
&lt;p&gt;$x_n \quad y_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的最大值。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 1
1 3
2 0
5 10
6 -10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
0 0
3 0
9 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;需要用到两数之和思想&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;单调滑动窗口&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/Dby_freedom/article/details/89066140&quot;&gt;【CSDN 博客】滑动窗口法总结&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://labuladong.online/algo/essential-technique/sliding-window-framework/&quot;&gt;【labuladong】滑动窗口核心代码模板&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2401_87820834/article/details/145998759&quot;&gt;【CSDN 博客】滑动窗口算法详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;单调栈与队列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/ds/monotonous-stack/&quot;&gt;【OI WiKi】单调栈相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://algo.itcharge.cn/03_stack_queue_hash_table/03_02_monotone_stack/&quot;&gt;【算法通关手册】单调栈详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2301_79248256/article/details/155377188&quot;&gt;【CSDN 博客】数据结构之单调栈&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/ds/monotonous-queue/&quot;&gt;【OI WiKi】单调队列相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/jerrycyx/p/18683014&quot;&gt;【Jerrycyx】实用而好写的数据结构&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/P2441M/p/18637702&quot;&gt;【P2441M】单调栈/单调队列&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】二分查找与二分答案</title><link>https://xingguang641.com/posts/acm/acm-note/binary-search-idea/binary-search-idea/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/binary-search-idea/binary-search-idea/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;二分查找基本原理&lt;/h1&gt;
&lt;p&gt;二分查找（Binary Search）是一种基于 &lt;strong&gt;分治思想&lt;/strong&gt; 的经典检索算法，专门用于在 &lt;strong&gt;有序序列&lt;/strong&gt; 中快速定位目标元素或特定边界。其核心逻辑建立在序列的单调性之上：通过对当前搜索区间的中点进行评估，算法能够根据评估值与目标值之间的逻辑关系，直接排除不可能包含解的其中一半区间。这种折半搜索的机制，使得搜索空间在每一轮迭代中都能以指数级速度缩减，从而在海量数据中实现快速锁定。相较于逐一比对的穷举法，二分查找将问题的时间复杂度降低为对数级，是处理大规模静态数据最有效的策略之一。&lt;/p&gt;
&lt;p&gt;在规模为 $n$ 的有序数组中，传统的顺序查找在最坏情况下需遍历整个序列，时间复杂度为 $O(n)$ ，而二分查找通过折半搜索的方式，将复杂度优化至 $O(\log n)$ 。这意味着即便数据规模从百万级跃升至十亿级，查找步数也仅从 $20$ 次增加到 $30$ 次左右。凭借这一显著的性能优势，二分查找不仅是算法竞赛中的核心工具，更是现代工程实践中构建检索系统的基石。&lt;/p&gt;
&lt;p&gt;在实际应用中，二分查找通常有四种标准形式，分别用于查找四种不同的逻辑边界：“最后一个 $&amp;lt; K$ 的位置” 、“第一个 $\geq K$ 的位置” 、“最后一个 $\le K$ 的位置” 以及 “第一个 $&amp;gt; K$ 的位置” 。尽管各形式的语义侧重不同，但其代码实现高度重叠，核心差异仅在于 &lt;strong&gt;比较条件的选取&lt;/strong&gt; 与 &lt;strong&gt;收缩策略的定义&lt;/strong&gt; 。从数轴分布的角度重新审视，这些变体本质上是在有序序列中寻找满足特定性质的 &lt;strong&gt;临界分割点&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cbinary-search-idea%5C%E5%9B%9B%E7%A7%8D%E4%BA%8C%E5%88%861.png&quot; alt=&quot;四种二分图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这四种变体看似繁杂，实则逻辑统一。理解的关键在于明确查找目标：是寻找满足条件的 &lt;strong&gt;最小索引&lt;/strong&gt; ，还是满足条件的 &lt;strong&gt;最大索引&lt;/strong&gt; 。通过精确控制左右指针的更新逻辑，我们可以确保搜索空间在每一步迭代中都向着目标边界收敛。接下来，我们将直接对比这四种形式具体的代码实现，并总结出一套通用的模版，帮助我们在处理不同的边界需求时，能够快速地写出准确无误的二分逻辑。&lt;/p&gt;
&lt;h2&gt;二分的代码详解&lt;/h2&gt;
&lt;p&gt;下文给出的四段代码分别对应前文所述的四种二分形式。为了确保 &lt;strong&gt;逻辑的严谨性&lt;/strong&gt; 与 &lt;strong&gt;实现的一致性&lt;/strong&gt; ，这些模型统一采用 &lt;strong&gt;闭区间 $[l, r]$&lt;/strong&gt; 的标准定义，并将 &lt;strong&gt;循环终止条件&lt;/strong&gt; 严格界定为 &lt;code&gt;l &amp;lt;= r&lt;/code&gt; 。在检索过程中，算法通过对区间中点的持续评估来驱动 &lt;strong&gt;左右边界的交替收缩&lt;/strong&gt; ，使搜索空间在每一轮迭代中缩减一半，直至区间完全收敛。为了能稳定地锁定目标位置，每种实现均引入了 &lt;strong&gt;辅助变量 ans&lt;/strong&gt; ，其核心作用是在满足判定条件时立刻暂存当前的候选坐标。这种设计确保了搜索空间在收敛为空集后，算法依然能够准确地输出 &lt;strong&gt;最后一次符合约束&lt;/strong&gt; 的有效索引。&lt;/p&gt;
&lt;p&gt;从代码编写的视角审视，这四类变体在 &lt;strong&gt;初始化逻辑&lt;/strong&gt; 、&lt;strong&gt;迭代步长&lt;/strong&gt; 以及 &lt;strong&gt;区间折半&lt;/strong&gt; 的计算方式上表现出高度的同质化，整体框架几乎完全一致。这种结构上的高度统一意味着我们只需掌握一套 &lt;strong&gt;核心模板&lt;/strong&gt; ，即可通过修改局部参数来适配不同的检索需求。其本质的差异化特征主要集中于两点：&lt;strong&gt;判定条件的具体选择&lt;/strong&gt; ，以及在条件达成时 &lt;strong&gt;对搜索方向的收缩决策&lt;/strong&gt; 。正是这些看似细微的逻辑差异，决定了算法指针最终会向哪一类 &lt;strong&gt;临界位置&lt;/strong&gt; 进行偏移。&lt;/p&gt;
&lt;h3&gt;最后一个 &amp;lt; K 的位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int lastLess(int a[], int n, int K) {
    int l = 0, r = n - 1, ans = -1;
    while (l &amp;lt;= r) {
        int m = (l + r) / 2;
        if (a[m] &amp;lt; K) {
            ans = m;
            l = m + 1;
        } else {
            r = m - 1;
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该二分用于查找 &lt;strong&gt;严格小于 K 的最大下标&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;lt; K&lt;/code&gt; 时，将当前下标 &lt;code&gt;m&lt;/code&gt; 记录到 &lt;code&gt;ans&lt;/code&gt; 中，并更新左边界&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;gt;= K&lt;/code&gt; 时，不更新 &lt;code&gt;ans&lt;/code&gt; ，并更新右边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若最终不存在满足条件的元素，则 &lt;code&gt;ans&lt;/code&gt; 保持为 &lt;code&gt;-1&lt;/code&gt; ，函数返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;第一个 ≥ K 的位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int firstGreaterEqual(int a[], int n, int K) {
    int l = 0, r = n - 1, ans = n;
    while (l &amp;lt;= r) {
        int m = (l + r) / 2;
        if (a[m] &amp;gt;= K) {
            ans = m;
            r = m - 1;
        } else {
            l = m + 1;
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该二分用于查找 &lt;strong&gt;大于等于 K 的最小下标&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;gt;= K&lt;/code&gt; 时，将当前下标 &lt;code&gt;m&lt;/code&gt; 记录到 &lt;code&gt;ans&lt;/code&gt; 中，并更新右边界&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;lt; K&lt;/code&gt; 时，不更新 &lt;code&gt;ans&lt;/code&gt; ，并更新左边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若不存在满足条件的元素，则 &lt;code&gt;ans&lt;/code&gt; 保持为 &lt;code&gt;n&lt;/code&gt; ，函数返回 &lt;code&gt;n&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;最后一个 ≤ K 的位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int lastLessEqual(int a[], int n, int K) {
    int l = 0, r = n - 1, ans = -1;
    while (l &amp;lt;= r) {
        int m = (l + r) / 2;
        if (a[m] &amp;lt;= K) {
            ans = m;
            l = m + 1;
        } else {
            r = m - 1;
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该二分用于查找 &lt;strong&gt;小于等于 K 的最大下标&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;lt;= K&lt;/code&gt; 时，将当前下标 &lt;code&gt;m&lt;/code&gt; 记录到 &lt;code&gt;ans&lt;/code&gt; 中，并更新左边界&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;gt; K&lt;/code&gt; 时，不更新 &lt;code&gt;ans&lt;/code&gt; ，并更新右边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若最终不存在满足条件的元素，则 &lt;code&gt;ans&lt;/code&gt; 保持为 &lt;code&gt;-1&lt;/code&gt; ，函数返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;第一个 &amp;gt; K 的位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int firstGreater(int a[], int n, int K) {
    int l = 0, r = n - 1;
    int ans = n;
    while (l &amp;lt;= r) {
        int m = (l + r) / 2;
        if (a[m] &amp;gt; K) {
            ans = m;
            r = m - 1;
        } else {
            l = m + 1;
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该二分用于查找 &lt;strong&gt;严格大于 K 的最小下标&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;gt; K&lt;/code&gt; 时，将当前下标 &lt;code&gt;m&lt;/code&gt; 记录到 &lt;code&gt;ans&lt;/code&gt; 中，并更新右边界&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;a[m] &amp;lt;= K&lt;/code&gt; 时，不更新 &lt;code&gt;ans&lt;/code&gt; ，并更新左边界&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若不存在满足条件的元素，则 &lt;code&gt;ans&lt;/code&gt; 保持为 &lt;code&gt;n&lt;/code&gt; ，函数返回 &lt;code&gt;n&lt;/code&gt; 。&lt;/p&gt;
&lt;h2&gt;二分的代码比较&lt;/h2&gt;
&lt;p&gt;通过对比四种二分形式的代码实现，我们可以发现它们在结构上高度统一：&lt;strong&gt;初始化逻辑&lt;/strong&gt; 、&lt;strong&gt;循环控制结构&lt;/strong&gt; 以及 &lt;strong&gt;闭区间表示方式&lt;/strong&gt; 完全一致。这种相似性意味着，我们在实际编写二分代码时，应将注意力集中在两个决定性的细节上：一是 &lt;strong&gt;判定条件与答案更新的时机&lt;/strong&gt; ，二是 &lt;strong&gt;搜索区间的收缩方向&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;先来看判断条件和答案的更新方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (a[m] &amp;lt; K) {
    ans = m;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然我们要查找的是 &lt;code&gt;&amp;lt; K&lt;/code&gt; ，那么判断条件就应当直接写成 &lt;code&gt;&amp;lt; K&lt;/code&gt; ，而不是通过其他形式绕一层逻辑。同时，&lt;code&gt;ans&lt;/code&gt; 的更新也必须放在这一判断条件内部，因为只有在 &lt;code&gt;a[m] &amp;lt; K&lt;/code&gt; 成立时，当前位置 &lt;code&gt;m&lt;/code&gt; 才是一个真实满足条件的位置，此时将其记录为候选答案是合理且安全的。&lt;/p&gt;
&lt;p&gt;这一点在其他几种二分中也是完全一致的：&lt;strong&gt;查什么条件，就在什么条件下更新答案&lt;/strong&gt; 。也就是说，判断语句中所写的比较关系，本身就直接对应了当前二分所要寻找的目标区间。只有在当前位置 &lt;code&gt;m&lt;/code&gt; 真正满足题目要求时，才会将其记录为候选答案；而在条件不满足的情况下，则通过调整区间边界继续查找，不对答案产生任何影响。保持这种规范的写法，可以避免条件与结果不匹配所带来的混乱。&lt;/p&gt;
&lt;p&gt;第二个关键点是区间的收缩方向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (a[m] &amp;lt; K) {
    ans = m;
    l = m + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当判定条件成立并记录下当前答案后，算法的下一步走向取决于我们要寻找的边界。若目标是寻找 &lt;strong&gt;最后一个&lt;/strong&gt; 满足条件的位置，即便当前 $m$ 已经符合要求，我们仍需确认右侧是否还有下标更大的合规位置，通过执行 $l = m + 1$ 来排除的左侧空间。反之，若目标是寻找 &lt;strong&gt;第一个&lt;/strong&gt; 满足条件的位置，即使 $m$ 已经符合要求，我们也必须向左侧继续逼近，以验证是否存在索引更小的合规位置，通过执行 $r = m - 1$ 来排除右侧空间。&lt;/p&gt;
&lt;p&gt;总的来说，这种处理边界的直觉可以概括为：&lt;strong&gt;求最后一个就向后搜索，求第一个就向前搜索&lt;/strong&gt; 。通过这种基于目标方位的动态调整，我们可以确保算法始终向预期的临界点逼近，从而避免返回位置出现偏差。只要明确了查找的目标，并根据该目标所在的方位来决定收缩方向，就能保证二分查找在各种场景下的正确性与稳定性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;二分查找基本性质&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/U243958&quot;&gt;双蛋问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=96214853&amp;amp;bvid=BV1KE41137PK&amp;amp;cid=164251653&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;最大值最小化问题&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最大值最小化问题&lt;/strong&gt; 是二分答案法最经典的应用场景之一。其核心思路在于，当直接构造最优方案较为困难时，我们可以将视角从方案本身转向对 &lt;strong&gt;答案范围&lt;/strong&gt; 的判定。通过二分枚举一个候选值作为上限约束，并检验在该约束下是否能找到可行方案，我们便能逐步逼近使最大代价尽可能小的最优解。这种转化将复杂的求解过程简化为了一系列简单的存在性判定，极大地降低了思维难度。&lt;/p&gt;
&lt;p&gt;从理论本质上看，&lt;strong&gt;最大值最小化问题一定可以用二分来解决&lt;/strong&gt; 。我们可以设定一个目标上界，只要能通过某种策略使所有元素都不超过这个值，系统的最大值就必然被该上界所压制。我们不断尝试调低这个上界，本质上就是在寻找系统承载能力的极限。如果上界为 $k$ 时无法达成目标，那么更严苛的约束条件也必然无法达成。这种单调性确保了可行解与不可行解之间存在明确的临界点，构成了二分搜索的逻辑基石。&lt;/p&gt;
&lt;p&gt;在实际编写代码时，这种思路提供了一种非常高效的解题范式。我们不再纠结如何直接构造出最优解，而只需要实现一个逻辑简单的贪心判定函数，用来验证给定约束下方案的可行性。利用二分查找，我们可以在极短的时间内精确锁定那个恰好满足条件的最小值。这种用间接验证代替直接求解的策略，不仅规避了复杂的逻辑推导，也是求解最大值最小化问题的核心手段。&lt;/p&gt;
&lt;h2&gt;分割数组最大值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/split-array-largest-sum/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个非负整数数组 $nums$ 和一个整数 $k$ ，你需要将这个数组分成 $k$ 个非空的连续子数组，使得这 $k$ 个子数组各自和的最大值 &lt;strong&gt;最小&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 1000$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^6$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq min{50, nums.length}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 2
7 2 5 10 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;18
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 2
1 2 3 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;该问题的背景是经典的 &lt;strong&gt;画匠问题&lt;/strong&gt; ，而画匠问题是 &lt;strong&gt;最大值最小化领域&lt;/strong&gt; 中最具代表性的经典案例。这类问题的核心挑战在于，如何在多名画匠协作且每人只能粉刷连续木板的前提下，让耗时最长的那位画匠工作量尽可能小，而这一问题的标志性解法正是 &lt;strong&gt;二分查找&lt;/strong&gt; 。之所以能够应用二分，是因为我们巧妙地利用了一个 &lt;strong&gt;决策函数&lt;/strong&gt; ，将复杂的 &lt;strong&gt;最优化问题&lt;/strong&gt; 转化为了一个具备严格单调性的 &lt;strong&gt;判定问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这种转化的逻辑基石在于：虽然直接构造一个完美的划分方案非常困难，但如果给定一个固定的时间上限 $X$ ，判断在 $K$ 个画匠内能否完成任务却非常简单。由于这个判定函数的返回值随 $X$ 的增大呈现出从 $\text{False}$ 到 $\text{True}$ 的 &lt;strong&gt;单调演变&lt;/strong&gt; ，这便为二分算法提供了施展空间。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;搜索空间&lt;/strong&gt;：二分查找不是作用在输入数据上，而是作用在 &lt;strong&gt;最终答案 X 的可能取值范围 $[L, R]$&lt;/strong&gt; 上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索目标&lt;/strong&gt;：我们的目标是找到使 $P(X)$ &lt;strong&gt;首次变为 $\text{True}$&lt;/strong&gt; 的那个最小值 $X_{\text{opt}}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这种 &lt;strong&gt;下压上界&lt;/strong&gt; 的策略，我们将原本可能需要复杂动态规划才能处理的线性划分问题，转化为简单的 &lt;strong&gt;贪心扫描&lt;/strong&gt; 。这种思维的转化方式，正是二分答案法在处理 &lt;strong&gt;最大值最小化&lt;/strong&gt; 问题时的精髓所在。它不仅简化了问题的计算维度，更利用了决策空间的单调性，将搜索效率推向了极致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通往奥格瑞玛城&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1462&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士，他是部落的中坚力量。有一天他醒来后发现自己居然到了联盟的主城暴风城。在被众多联盟的士兵攻击后，他决定逃回自己的家乡奥格瑞玛。&lt;/p&gt;
&lt;p&gt;在艾泽拉斯，有 $n$ 个城市，编号为 $1, 2, 3, \ldots, n$ 。城市之间有 $m$ 条双向的公路。每经过一条公路，都会遭到攻击并损失一定的血量。此外，每当进入一个城市（包括起点和终点），都会被收取一定的费用。&lt;/p&gt;
&lt;p&gt;当前掌握的信息如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;城市 $1$ 为暴风城，城市 $n$ 为奥格瑞玛；&lt;/li&gt;
&lt;li&gt;术士的最大血量为 $b$ ，出发时血量是满的；&lt;/li&gt;
&lt;li&gt;若在途中血量降为负数，则无法到达终点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;术士希望尽量少花钱。现在需要找到一条从 $1$ 到 $n$ 的路径，使得在血量不降为负的前提下，&lt;strong&gt;路径上经过城市的单次收费最大值尽可能小&lt;/strong&gt; 。如果无法到达奥格瑞玛，则输出 &lt;code&gt;AFK&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 5 \times 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq b \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq c_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;$0 \leq f_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;可能存在重边&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行三个整数 $n$ 、$m$ 和 $b$ 。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行一个整数 $f_i$ ，表示经过城市 $i$ 需要支付的费用。&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行，每行三个整数 $a_i, b_i, c_i$ ，表示城市 $a_i$ 与 $b_i$ 之间有一条双向公路，通过会损失 $c_i$ 的血量。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m \quad b$&lt;/p&gt;
&lt;p&gt;$f_1$&lt;/p&gt;
&lt;p&gt;$f_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$f_n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad b_1 \quad c_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$a_m \quad b_m \quad c_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示路径上单次城市收费最大值的最小可能值。如果无法到达城市 $n$ ，输出 &lt;code&gt;AFK&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 4 8
8
5
6
10
2 1 2
2 4 1
1 3 4
3 4 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;最大值最小化问题的另一个经典就是 &lt;strong&gt;路径中最大边权最小化问题&lt;/strong&gt; ，常见的做法就是二分或者增量法（从小边权开始加边，直到两点连通）。这一类问题的共同特点是：答案本身具有明显的 &lt;strong&gt;单调性&lt;/strong&gt; ，当允许的最大权值越大时，可行的方案只会越来越多。因此我们可以通过枚举最大允许值，并判断是否存在合法方案来求解。&lt;/p&gt;
&lt;p&gt;题目要求从城市 $1$ 走到城市 $n$ ，每经过一个城市需要支付费用 $f_i$ ，同时每条道路会消耗一定血量，而总血量损失不能超过 $b$ 。如果我们把路径中经过城市的费用最大值记为 $x$ ，那么问题就变成：&lt;strong&gt;在只允许经过 $f_i \leq x$ 的城市的情况下，是否还能从 1 走到 n，并且总血量损失不超过 b&lt;/strong&gt; 。这时可以发现一个非常关键的性质：如果某个 $x$ 是可行的，那么任何 $x&apos; &amp;gt; x$ 也一定是可行的，因为允许经过的城市只会更多，不会更少。这种明显的单调关系正好满足二分答案的条件。因此我们可以对路径中允许的最大城市费用进行二分。&lt;/p&gt;
&lt;p&gt;在具体实现时，假设当前二分到的最大费用为 $mid$ 。我们只保留所有满足 $f_i \leq mid$ 的城市，其余城市视为不可进入。然后在这个限制下的图中，从城市 $1$ 出发计算到城市 $n$ 的最短路，边权就是道路的血量损失。如果求出的最短距离不超过 $b$ ，说明在最大费用为 $mid$ 的限制下仍然存在一条合法路径，那么这个答案就是可行的，可以继续尝试更小的费用；反之如果最短路已经超过 $b$ 或者无法到达终点，则说明当前限制过于严格，需要增大 $mid$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;离线型中位数问题&lt;/h1&gt;
&lt;p&gt;二分答案法在 &lt;strong&gt;求解中位数或第 k 小值&lt;/strong&gt; 的问题中同样非常有效。对于这些静态、离线处理的场景，我们无需直接寻找答案本身，而是对 &lt;strong&gt;答案的取值空间&lt;/strong&gt; 进行二分。每当二分到一个候选数 $x$ 时，只需统计有多少元素小于或等于 $x$ ，就能判断 $x$ 是否满足第 $k$ 小值的要求。通过不断缩小搜索区间，最终便可以锁定真实的中位数或第 $k$ 小值。&lt;/p&gt;
&lt;p&gt;这种思路其实用到了&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/difference-idea/#%E5%B7%AE%E5%88%86%E7%9A%84%E5%B9%BF%E4%B9%89%E8%A7%86%E8%A7%92&quot;&gt;广义差分&lt;/a&gt;的逻辑。直接定位中位数是一个极其严苛的点约束，要求数值在全局排序中精确对位，而二分答案则将这一硬性条件拆解为更宽松的范围统计，即计算 “小于等于 $x$ 的元素个数” 。这种以面求点的方式，本质上是将复杂的全局序关系简化为数值在值域区间上的贡献分布。通过判断计数值是否超过所需总数，利用单调性不断逼近真相，将原本难以处理的单值查询转化为简单的计数查询。&lt;/p&gt;
&lt;p&gt;在一些更复杂的题目中，仅仅统计 $\leq x$ 的数量还不够，往往还需要结合 &lt;strong&gt;二值化（01 化）技巧&lt;/strong&gt; 。具体来说，可以将数组中的元素根据当前二分值 $x$ 进行转化，例如把满足条件的元素映射为 $1$ ，不满足的映射为 $-1$ 或 $0$ 。这样原本的 &lt;strong&gt;中位数判定问题&lt;/strong&gt; 就可以转化为 &lt;strong&gt;前缀和或子段和是否满足某种性质的问题&lt;/strong&gt; ，从而借助前缀和、树状数组或平衡树等数据结构高效完成统计。若想具体了解二值化技巧，可以看&lt;a href=&quot;https://www.cnblogs.com/dbywsc/p/19065445&quot;&gt;这篇博客&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;而在需要&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/priority-queue/#%E6%95%B0%E6%8D%AE%E6%B5%81%E4%B8%AD%E4%BD%8D%E6%95%B0%E9%97%AE%E9%A2%98&quot;&gt;动态维护中位数&lt;/a&gt;的场景中，通常会采用 &lt;strong&gt;堆顶对&lt;/strong&gt; 的结构：用一个最大堆维护较小的一半元素，用一个最小堆维护较大的一半元素。这样在每次插入新元素后，都能迅速得到当前的中位数。此外，当题目要求 &lt;strong&gt;输出前 k 个最优答案&lt;/strong&gt; 时，也需要使用优先队列。这类问题通常会维护一个优先队列来持续扩充当前最优状态，每次取出当前最小（或最大）的候选解，并生成新的候选结果。通过这种方式，可以按顺序得到前 $k$ 个答案，而无需枚举所有可能情况，大大降低时间复杂度。&lt;/p&gt;
&lt;h3&gt;离线型平均数问题&lt;/h3&gt;
&lt;p&gt;与中位数类似，&lt;strong&gt;平均数最大化或最小化&lt;/strong&gt; 的问题也可以通过 &lt;strong&gt;二分答案法&lt;/strong&gt; 来解决。设数组为 $A_1,A_2,\ldots,A_n$ ，如果题目要求在某种约束条件下找到 &lt;strong&gt;最大平均值&lt;/strong&gt; ，我们通常并不会直接计算平均数，而是对答案 $x$ 进行二分。对于每一个候选值 $x$ ，我们只需要判断是否存在一个合法的选择方案，使得该方案的平均值 &lt;strong&gt;不小于 x&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;平均值问题的关键在于一个常见的代数变形，若某个集合 $S$ 的平均值满足：&lt;/p&gt;
&lt;p&gt;$$
\frac{\sum_{i \in S} A_i}{|S|} \geq x
$$&lt;/p&gt;
&lt;p&gt;则原式等价于：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i \in S} (A_i - x) \geq 0
$$&lt;/p&gt;
&lt;p&gt;这样一来，只需要将原数组进行一次 &lt;strong&gt;线性变换 $B_i = A_i - x$&lt;/strong&gt; ，原问题就转化为 &lt;strong&gt;是否存在一个合法集合 S，使得数组累加和大于 0&lt;/strong&gt; 。此时问题已经不再涉及平均值，而变成了一个 &lt;strong&gt;最大子段和或最大权值选择问题&lt;/strong&gt; ，通常可以通过动态规划或使用数据结构来判断。这一技巧在算法竞赛中非常常见，通过将平均值转化为线性和的形式，可以有效避免直接处理分数带来的复杂性，从而使问题可以使用常见的数据结构和动态规划方法解决。&lt;/p&gt;
&lt;h2&gt;第K小数对距离&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-k-th-smallest-pair-distance/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;数对 &lt;code&gt;(a, b)&lt;/code&gt; 由整数 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 组成，其数对距离定义为 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 的绝对差值。&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 $k$ ，数对由 &lt;code&gt;nums[i]&lt;/code&gt; 和 &lt;code&gt;nums[j]&lt;/code&gt; 组成且满足 $0 &amp;lt;= i &amp;lt; j &amp;lt; n$ ，$n$ 表示数组长度。返回 &lt;strong&gt;所有数对距离中&lt;/strong&gt; 第 &lt;code&gt;k&lt;/code&gt; 小的数对距离。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq n \leq 10^4$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^6$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq n * (n - 1) / 2$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 1
1 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
1 6 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的目标是找到所有数对距离中的 &lt;strong&gt;第 k 小值&lt;/strong&gt; 。由于数对的总数达到了 $O(n^2)$ 级别，直接枚举并排序在 $n=10^4$ 的数据规模下会超时。因此，我们需要转换思路，将问题看作是对 &lt;strong&gt;距离值域&lt;/strong&gt; 的二分查找。我们并不直接去数对中寻找目标，而是通过二分一个候选距离 $X$ ，来判定真实答案与 $X$ 的位置关系。&lt;/p&gt;
&lt;p&gt;这种转化的核心逻辑在于统计 &lt;strong&gt;距离小于或等于 X 的数对数量&lt;/strong&gt; 。如果这个数量大于或等于 $k$ ，说明我们设定的距离 $X$ 已经覆盖了足够多的数对，第 $k$ 小的距离一定就在 $X$ 或者比 $X$ 更小的范围内，此时我们记录答案并尝试向左缩小距离上限。反之，如果数量小于 $k$ ，则说明 $X$ 定得太小，目标距离一定在 $X$ 的右侧。&lt;/p&gt;
&lt;p&gt;为了快速完成统计，我们可以先对数组进行排序。在有序序列中，利用 &lt;strong&gt;双指针&lt;/strong&gt; 技巧可以在 $O(n)$ 的时间内扫描出所有满足条件的数对，而无需嵌套循环。通过在 $0$ 到数组最大差值之间进行二分，配合这个高效的计数函数，我们就能在对数时间内精准锁定第 $k$ 小的数对距离。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;唯一数组中位数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-the-median-of-the-uniqueness-array/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 。数组 &lt;code&gt;nums&lt;/code&gt; 的 &lt;strong&gt;唯一性数组&lt;/strong&gt; 是一个按元素从小到大排序的数组，包含了 &lt;code&gt;nums&lt;/code&gt; 的所有非空子数组中不同元素的个数。&lt;/p&gt;
&lt;p&gt;换句话说，这是由所有 $0 \leq i \leq j &amp;lt; nums.length$ 的 &lt;code&gt;distinct(nums[i..j])&lt;/code&gt; 组成的递增数组。&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;distinct(nums[i..j])&lt;/code&gt; 表示从下标 $i$ 到下标 $j$ 的子数组中不同元素的数量。&lt;/p&gt;
&lt;p&gt;返回 &lt;code&gt;nums&lt;/code&gt; 唯一性数组的中位数。&lt;/p&gt;
&lt;p&gt;注意，数组的中位数定义为有序数组的中间元素。如果有两个中间元素，则取值较小的那个。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i] \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示唯一性数组的中位数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
3 4 3 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
4 3 5 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;平均数与中位数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc236/tasks/abc236_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $N$ 张卡片，第 $i$ 张卡片上写着一个整数 $A_i$ 。高桥可以从这些卡片中选择任意多张，但必须满足一个条件：对于每个 $i$ ，第 $i$ 张卡片和第 $i+1$ 张卡片 &lt;strong&gt;至少要选择一张&lt;/strong&gt; 。也就是说，&lt;strong&gt;不允许存在两个相邻的卡片都不被选择&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在所有满足条件的选择方案中，请求出：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所选卡片上数字的 &lt;strong&gt;平均值的最大可能值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;所选卡片上数字的 &lt;strong&gt;中位数的最大可能值&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要注意的是：中位数定义为上中位数，平均数只要与正确答案的 &lt;strong&gt;误差不超过 $10^{-3}$&lt;/strong&gt; 即可视为正确。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示卡片数量。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示每张卡片上的数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行输出所选卡片数字的 &lt;strong&gt;最大可能平均值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;第二行输出所选卡片数字的 &lt;strong&gt;最大可能中位数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
2 1 2 1 1 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
3 1 4 1 5 9 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5.250000000
4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-problems/maximum-subarray-sum/#%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D%E5%AF%B9%E5%81%B6%E9%97%AE%E9%A2%98&quot;&gt;打家劫舍对偶问题&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;逆向分析求解问题&lt;/h1&gt;
&lt;p&gt;在算法竞赛的解题过程中，如果沿着题意正向推导，往往会面临难以处理的动态约束，此时&lt;strong&gt;逆向求解&lt;/strong&gt; 便成了一种极为方便的解题思路。其核心逻辑在于：我们不再纠结如何直接构造出最优解，而是先假设一个候选答案 $x$ ，随后尝试判断这个 $x$ 是否能够满足题目给出的所有约束条件。&lt;/p&gt;
&lt;p&gt;这种先猜后验的思想能够演变为经典的 &lt;strong&gt;二分答案法&lt;/strong&gt; ，主要依赖两个前置条件。首先，题目通常带有极强的 &lt;strong&gt;最值信号&lt;/strong&gt; ，如 “最大/最小” 、“至多/至少” 等字眼。当问题的目标是逼近某个临界点，逆向验证的价值才会凸显。其次，解空间必须具备 &lt;strong&gt;单调性&lt;/strong&gt; 。随着候选答案 $x$ 的增减，满足条件的可能性必须呈现单向变化。若设定上界为 $k$ 时方案可行，那么放宽至 $k+1$ 也必然可行。这种性质确保了我们可以通过不断调整限制，在对数时间内锁定判定函数从 $\text{False}$ 跃迁至 $\text{True}$ 的临界点。&lt;/p&gt;
&lt;p&gt;通过这种逆向思考，我们将棘手的 &lt;strong&gt;最优化问题&lt;/strong&gt; 降级为了简单的 &lt;strong&gt;判定性问题&lt;/strong&gt; 。这种重心转移极大地简化了代码实现的难度：我们不需要在复杂的约束中寻找最优解法，而是从答案出发，通过不断的逻辑校验来缩小搜索空间。这种逆向求解的思路，是解决高难度约束优化问题的核心套路。&lt;/p&gt;
&lt;h2&gt;刀砍与毒杀怪兽&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algorithmzuo/algorithm-journey/blob/main/src/class051/Code07_CutOrPoison.java&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;怪兽的初始血量是一个整数 $hp$ ，给出每一回合刀砍和毒杀的数值 $cuts$ 和 $poisons$ 。第 $i$ 回合如果用刀砍，怪兽在这回合会直接损失 $cuts[i]$ 的血，不再有后续效果；第 $i$ 回合如果用毒杀，怪兽在这回合不会损失血量，但是之后每回合都损失 $poisons[i]$ 的血量，并且所有毒杀效果可以叠加。&lt;/p&gt;
&lt;p&gt;两个数组 $cuts$ 、$poisons$ ，长度都是 $n$ ，代表你一共可以进行 $n$ 回合。每一回合你只能选择刀砍或者毒杀中的一个动作，如果你在 $n$ 个回合内没有直接杀死怪兽，意味着你已经无法有新的行动了。但是怪兽如果有中毒效果的话，那么怪兽依然会在血量耗尽的那回合死掉。&lt;/p&gt;
&lt;p&gt;返回至少多少回合，怪兽会死掉。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq hp \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq cuts[i], poisons[i] \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个个整数 $n$ 和 $hp$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示 $cuts$ 数组中的元素。&lt;/li&gt;
&lt;li&gt;第三行包含 $n$ 个整数，表示 $poisons$ 数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad hp$&lt;/p&gt;
&lt;p&gt;$cuts_1 \quad cuts_2 \quad \ldots \quad cuts_n$&lt;/p&gt;
&lt;p&gt;$poisons_1 \quad poisons_2 \quad \ldots \quad poisons_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的核心难点在于 &lt;strong&gt;伤害收益的动态变化&lt;/strong&gt; 。刀砍产生的伤害是即时的固定值，而毒杀的收益则取决于后续还能持续多少个回合。在不确定总回合数的情况下，我们很难在每一回合做出最优选择。但如果我们转换思路，假设已知怪兽会在第 $T$ 个回合死掉，那么第 $i$ 回合选择毒杀所能提供的总伤害就固定为 $(T - i) \times poisons[i]$ 。&lt;/p&gt;
&lt;p&gt;这种性质使得问题具备了明显的 &lt;strong&gt;单调性&lt;/strong&gt;：如果怪兽能在 $T$ 个回合内被击败，那么在更长的回合数（如 $T+1$ ）里也一定能被击败。反之，如果 $T$ 个回合不够，那么更短的时间内也绝对无法达成目标。基于这种单调性，我们可以通过 &lt;strong&gt;二分答案&lt;/strong&gt; 来搜索最小的达标回合数。&lt;/p&gt;
&lt;p&gt;在具体的判定逻辑中，当我们假定总时长为 $T$ 时，每一回合的选择就变得非常简单：只需要对比当前回合 “刀砍的固定伤害” 与 “毒杀直到第 $T$ 回合能累积的总伤害” 并直接选择较大者即可。通过贪心地累加每一回合的最优伤害，再判断总和是否达到怪兽的血量 $hp$ ，我们就能在对数时间内锁定最少的回合数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;打家劫舍问题IV&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber-iv&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。由于相邻的房屋装有相互连通的防盗系统，所以小偷 &lt;strong&gt;不会窃取相邻的房屋&lt;/strong&gt; 。小偷的 &lt;strong&gt;窃取能力&lt;/strong&gt; 定义为他在窃取过程中能从单间房屋中窃取的 &lt;strong&gt;最大金额&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 表示每间房屋存放的现金金额。形式上，从左起第 &lt;code&gt;i&lt;/code&gt; 间房屋中放有 &lt;code&gt;nums[i]&lt;/code&gt; 美元。另给你一个整数 &lt;code&gt;k&lt;/code&gt; ，表示窃贼将会窃取的 &lt;strong&gt;最少&lt;/strong&gt; 房屋数。小偷总能窃取至少 &lt;code&gt;k&lt;/code&gt; 间房屋。&lt;/p&gt;
&lt;p&gt;返回小偷的 &lt;strong&gt;最小&lt;/strong&gt; 窃取能力。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$0 \leq k \leq (nums.length + 1) / 2$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 2
2 3 5 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 2
2 7 9 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;h2&gt;饥饿的高桥先生&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc415/tasks/abc415_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个 $H \times W$ 的网格，每个单元格 $(i, j)$ 上都有 $A_{i, j}$ 枚硬币。高桥初始位于 $(1, 1)$ 且持有 $x$ 枚硬币。&lt;/p&gt;
&lt;p&gt;高桥需要在 $H + W - 1$ 天内完成移动，规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;收集硬币&lt;/strong&gt;：高桥到达一个单元格时，收集该单元格上的所有 $A_{i, j}$ 枚硬币。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;购买食物&lt;/strong&gt;：在每一天（即到达一个新的单元格后），他必须消耗 $P_k$ 枚硬币购买食物。如果手中持有的硬币少于 $P_k$ ，他将因饥饿倒下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行动逻辑&lt;/strong&gt;：如果 $k &amp;lt; H + W - 1$ ，高桥可以选择向下移动或向右移动。如果 $k = H + W - 1$ ，他停留在当前位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请找出使高桥能够不因饥饿而完成所有 $H+W-1$ 天移动的最小非负整数 $x$。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq H, W \leq 100$&lt;/li&gt;
&lt;li&gt;$1 \leq A_{i, j} \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq P_k \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $H$ 和 $W$ 。&lt;/li&gt;
&lt;li&gt;接下来 $H$ 行，每行包含 $W$ 个整数，表示网格各单元格的硬币数量 $A_{i, j}$ 。&lt;/li&gt;
&lt;li&gt;最后一行包含 $H + W - 1$ 个整数，表示每天需消耗的硬币数量 $P_i$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$H \quad W$&lt;/p&gt;
&lt;p&gt;$A_{1, 1} \quad A_{1, 2} \quad \ldots \quad A_{1, W}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_{H, 1} \quad A_{H, 2} \quad \ldots \quad A_{H, W}$&lt;/p&gt;
&lt;p&gt;$P_1 \quad P_2 \quad \ldots \quad P_{H+W-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件所需的最小初始硬币数量。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 2
3 1
4 1
1 3 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;反向DP 或 二分+正向DP&lt;/p&gt;
&lt;h2&gt;可安排的任务数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-number-of-tasks-you-can-assign/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你 $n$ 个任务和 $m$ 个工人。每个任务需要一定的力量值才能完成，需要的力量值保存在下标从 0 开始的整数数组 $tasks$ 中，第 $i$ 个任务需要 $tasks[i]$ 的力量才能完成。每个工人的力量值保存在下标从 $0$ 开始的整数数组 $workers$ 中，第 $j$ 个工人的力量值为 $workers[j]$ 。&lt;/p&gt;
&lt;p&gt;每个工人只能完成 &lt;strong&gt;一个&lt;/strong&gt; 任务，且力量值需要 &lt;strong&gt;大于等于&lt;/strong&gt; 该任务的力量要求值（即 $workers[j] &amp;gt;= tasks[i]$ ）。除此以外，你还有 $pills$ 个神奇药丸，可以给 &lt;strong&gt;一个工人的力量值&lt;/strong&gt; 增加 $strength$ 。你可以决定给哪些工人使用药丸，但每个工人 &lt;strong&gt;最多&lt;/strong&gt; 只能使用 &lt;strong&gt;一片&lt;/strong&gt; 药丸。&lt;/p&gt;
&lt;p&gt;请你返回 &lt;strong&gt;最多&lt;/strong&gt; 有多少个任务可以被完成。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$n == tasks.length$&lt;/li&gt;
&lt;li&gt;$m == workers.length$&lt;/li&gt;
&lt;li&gt;$1 \leq n, m \leq 5 * 10^4$&lt;/li&gt;
&lt;li&gt;$0 \leq pills \leq m$&lt;/li&gt;
&lt;li&gt;$0 \leq tasks[i], workers[j], strength \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含三行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含四个整数 $n$ 、$m$ 、$pills$ 和 $strength$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示 $tasks$ 数组中的元素。&lt;/li&gt;
&lt;li&gt;第三行包含 $m$ 个整数，表示 $workers$ 数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m \quad pills \quad strength$&lt;/p&gt;
&lt;p&gt;$tasks_1 \quad tasks_2 \quad \ldots \quad tasks_n$&lt;/p&gt;
&lt;p&gt;$workers_1 \quad workers_2 \quad \ldots \quad workers_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3 1 1
3 2 1
0 3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 3 1 5
5 4
0 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 5 3 10
10 15 30
0 10 10 10 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;首先思考一下这道题的简单版本：假设 $tasks$ 数组和 $works$ 数组都是有序的，并且不存在大力药丸。那我们就可以直接用 &lt;strong&gt;双指针匹配&lt;/strong&gt; 的思路来解决：如果当前工人的力量能够完成当前任务，就让他做该任务，否则跳过这个工人。整个过程就是工人与任务 &lt;strong&gt;按顺序逐一配对&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在重新加入大力药丸之后，我们很容易会想到继续沿用上面的双指针策略：如果一个工人可以做当前的的任务，我们就让这个工人做当前的任务；如果不可以，我们就尝试让该工人吃药，再尝试完成当前的任务。这一思路看似合理，但其实是 &lt;strong&gt;错误的&lt;/strong&gt; ，原因在于：当工人吃药之后，他在全体工人中的相对排序也随之改变。既然实力变强，他理论上就应该去承担更高难度的任务，而不再适合停留在原本的任务位置。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们可以尝试一个更合理的贪心：当一个工人能够胜任多个任务时，让他去完成 &lt;strong&gt;他能力范围内最困难的任务&lt;/strong&gt; 。这是因为困难的任务可供选择的工人本来就更少，因此应当优先分配。可以轻易证明这个策略比按顺序逐一配对的思路更优。这就是所谓的&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/heap-problems/greedy-algorithm/#%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%B4%AA%E5%BF%83%E9%97%AE%E9%A2%98&quot;&gt;解锁任务&lt;/a&gt;的思路：对于每个工人，我们把所有他能做的任务加入一个任务集合，并优先分配其中最难的任务。&lt;/p&gt;
&lt;p&gt;据此我们又可以尝试如下策略：如果当前工人完全做不了任务，那就让他吃药，再尝试解锁任务；如果仍无法完成，则撤销药丸并继续尝试下一个工人。不过这条看似严谨的贪心策略依然是 &lt;strong&gt;错误的&lt;/strong&gt;：如果工人的能力数组为 $[2,3,4]$ ，任务的难度数组为 $[3,8,9]$ ，药丸只有一个并且药效为 &lt;code&gt;+5&lt;/code&gt; 点能力值。那么我们直接给第一个工人吃药就只能做一个任务，而给第三个工人吃药则可以做两个任务。也就是说，我们无法确定哪个工人一定得吃药。&lt;/p&gt;
&lt;p&gt;但如果我们事先就知道要完成的任务数量 $ans$ ，情况就会立刻变得明确。根据自然智慧，我们应当直接选择难度最低的 $ans$ 个任务和能力最大的 $ans$ 个工人。由于我们一定要完成 $ans$ 个任务，也就是每个被选中的工人都必须完成一个任务。因此在某个工人不吃药就无任务可做的情况下，我们就必须让他吃药。这样一来，我们就解决了正向思路中 “究竟该不该给某个工人吃药” 这一难以判断的问题。&lt;/p&gt;
&lt;p&gt;这种判定逻辑具有天然的 &lt;strong&gt;单调性&lt;/strong&gt;：要求的任务数量越多，达成目标的难度就越高；反之，任务数量越少，达成目标的难度就越低。基于这一单调性，我们可以对完成任务的数量进行二分搜索。在每一轮判定中，我们只需要验证在给定的药丸限制下，力量最大的 $T$ 个工人能否处理难度最低的 $T$ 个任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/basic/binary/&quot;&gt;【OI WiKi】二分查找相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_45978890/article/details/116094046&quot;&gt;【CSDN 博客】二分查找超级详细的图解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/MarisaMagic/p/17093253.html&quot;&gt;【MarisaMagic】二分查找的四种写法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/92288604&quot;&gt;【知乎专栏】动态规划问题之高楼扔鸡蛋&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【ACM 算法随笔】差分数组与差分思想</title><link>https://xingguang641.com/posts/acm/acm-note/difference-idea/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/difference-idea/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;差分数组基本原理&lt;/h1&gt;
&lt;p&gt;差分是一种 &lt;strong&gt;常用且高效&lt;/strong&gt; 的数组处理技巧，其核心在于将 &lt;strong&gt;原本作用在整个区间上的线性更新&lt;/strong&gt; ，巧妙地转化成 &lt;strong&gt;区间端点的局部常数级操作&lt;/strong&gt; 。在处理带有频繁区间修改的问题时，直接维护原数组往往会造成 $O(nq)$ 的高额开销，而差分数组通过记录 &lt;strong&gt;相邻元素之间的变化&lt;/strong&gt; ，替代了原本的线性更新。我们定义差分数组：&lt;/p&gt;
&lt;p&gt;$$
d[i] = a[i] - a[i-1]
$$&lt;/p&gt;
&lt;p&gt;这意味着原数组中的每一个元素都可以通过对差分数组 &lt;strong&gt;求前缀和&lt;/strong&gt; 恢复出来。差分的关键优势在于，它允许我们在 &lt;strong&gt;仅修改两个位置&lt;/strong&gt; 的前提下完成对 &lt;strong&gt;整个区间&lt;/strong&gt; 的批量更新。例如，若需将区间 $[l, r]$ 内的所有元素同步增加 $x$ ，在差分数组中仅需执行 &lt;code&gt;d[l] += x&lt;/code&gt; 以标记变化的开始，并在 &lt;code&gt;d[r+1] -= x&lt;/code&gt; 处标记变化的结束。&lt;/p&gt;
&lt;p&gt;这种机制将单次区间更新的时间复杂度从线性的 $O(n)$ &lt;strong&gt;直接降至常数级的 $O(1)$&lt;/strong&gt; 。在算法设计中，差分不仅是批量区间修改问题的利器，更是构造前缀和结构、处理扫描线类问题的底层逻辑，是一种将全局更新转化为局部增量的简洁而高效的基础技巧。&lt;/p&gt;
&lt;h2&gt;差分的狭义视角&lt;/h2&gt;
&lt;p&gt;从数学本质上看，差分可以被视为 &lt;strong&gt;离散意义下的导数&lt;/strong&gt; 。在连续数学中，导数刻画的是函数在瞬时状态下的变化率；而在离散的计算逻辑中，由于无法进行极限运算，我们便利用相邻差来描述序列的变化趋势：&lt;/p&gt;
&lt;p&gt;$$
d[i] = a[i] - a[i-1]
$$&lt;/p&gt;
&lt;p&gt;这一映射关系完美对应了连续函数中倒数的近似关系：&lt;/p&gt;
&lt;p&gt;$$
f&apos;(x) \approx f(x) - f(x-1)
$$&lt;/p&gt;
&lt;p&gt;在这种视角下，&lt;strong&gt;原数组描述的是系统的状态&lt;/strong&gt; ，而 &lt;strong&gt;差分数组描述的是状态的变化&lt;/strong&gt; 。对差分数组求前缀和的过程，本质上等同于对离散导数 &lt;strong&gt;进行一次积分&lt;/strong&gt; ，从而由变化率还原出原函数的状态轨迹。&lt;/p&gt;
&lt;p&gt;这种思路的强大之处在于，它让我们不再盯着最终的数组结果看，而是先去处理每一次操作带来的增量。不管是频繁修改一段区间的值，还是统计多个重叠区域的覆盖情况，核心逻辑都是一致的：&lt;strong&gt;先把所有的变化量记录在差分数组里，最后再通过一次前缀和扫描还原出全局的结果&lt;/strong&gt; 。这种化繁为简的策略，不仅大幅降低了重复计算的开销，也构成了处理区间类问题最基础、最实用的思维模板。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体思路可以看 N 神的视频讲解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113927966954406&amp;amp;bvid=BV1dCFfemEHX&amp;amp;cid=28173994960&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;差分的广义视角&lt;/h2&gt;
&lt;p&gt;在很多算法问题中，我们经常会遇到一种情况：题目所要求的条件本身并不方便直接计算，但如果稍微改变一下 &lt;strong&gt;统计的方式或表达的形式&lt;/strong&gt; ，问题就会立刻变得容易处理。这类通过 &lt;strong&gt;改变统计对象或约束表达&lt;/strong&gt; 来化简问题的思路，可以从更宽泛的角度理解为一种 &lt;strong&gt;广义的差分思想&lt;/strong&gt; 。它并不局限于数组中的相邻元素之差，而是更强调通过 &lt;strong&gt;构造两个更容易计算的量，再通过差分得到目标结果&lt;/strong&gt; ，从而规避直接处理复杂条件的困难。&lt;/p&gt;
&lt;p&gt;在这种思路下，我们往往不会直接去求目标答案，而是先寻找一些 &lt;strong&gt;更容易计算的辅助函数或统计量&lt;/strong&gt; 。当问题被重新表达之后，原本精确而复杂的条件有时可以被替换为 &lt;strong&gt;更宽松、更单调的限制&lt;/strong&gt; ，或者被改写成 &lt;strong&gt;更容易维护的结构&lt;/strong&gt; 。最终再通过简单的相减或差分操作，就能够恢复出原问题的答案。这样的转化本质上是一种 &lt;strong&gt;“先放宽条件、再通过差分恢复精确结果”&lt;/strong&gt; 的过程。&lt;/p&gt;
&lt;p&gt;在很多经典算法模型中，都可以看到这种思想的影子。例如，有时我们会把 &lt;strong&gt;恰好满足某个条件的计数&lt;/strong&gt; 转化为两个 &lt;strong&gt;不超过某个上界的计数函数之差&lt;/strong&gt;；有时会把 &lt;strong&gt;区间上的信息&lt;/strong&gt; 改写为 &lt;strong&gt;前缀形式&lt;/strong&gt; 来进行计算；还有一些原本需要同时考虑 &lt;strong&gt;上下边界的范围条件&lt;/strong&gt; 的问题，也可以通过差分转化为 &lt;strong&gt;只需要处理单侧限制&lt;/strong&gt; 的形式。虽然这些技巧在表面上属于不同的知识点，但如果从更抽象的角度来看，它们都体现了同一种核心思路：&lt;strong&gt;通过改变问题的表达方式，使答案可以由两个更简单的问题之差得到&lt;/strong&gt; 。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;差分构造题目收集&lt;/h1&gt;
&lt;p&gt;在处理区间增减操作时，直接观察原数组往往会陷入 &lt;strong&gt;全局耦合的困局&lt;/strong&gt; 。由于区间操作在空间上存在高度重叠，每个位置的最终数值均是多次操作叠加的结果，这种复杂的相互作用使得 &lt;strong&gt;操作顺序的梳理与方案的构造&lt;/strong&gt; 极具挑战。然而，此类问题的本质并非数组的绝对数值，而是 &lt;strong&gt;相邻位置间变化量的生成机制&lt;/strong&gt; 。通过引入差分序列 $d_i = a_i - a_{i-1}$，我们可以将原本全局性的区间覆盖精确地转化为 &lt;strong&gt;局部性的端点变动&lt;/strong&gt; ，对区间 $[l, r]$ 的整体操作，在差分域中仅表现为 $d_l$ 与 $d_{r+1}$ 两个孤立点的数值修正。&lt;/p&gt;
&lt;p&gt;当视角切换至差分域后，关于原数组的构造问题便精确地转化为：&lt;strong&gt;是否存在一组满足特定约束的差分序列，使其通过前缀和还原后能与目标数组精确对齐&lt;/strong&gt; 。在这种逻辑框架下，棘手的区间干涉消失了，取而代之的是对差分序列取值范围、正负分布以及代数守恒关系的分析。大多数题目最终都会转化为对差分序列总和的校验，或者是对端点数值抵消的匹配构造。&lt;/p&gt;
&lt;p&gt;从算法特性的角度审视，差分数组的核心优势在于其 &lt;strong&gt;静态验证的高效性&lt;/strong&gt; 。差分技巧常用于多修改、单查询的场景，而构造类题目恰好契合这一特性，其核心诉求往往是在执行完一系列潜在操作后，进行 &lt;strong&gt;仅有一次的最终验证或方案输出&lt;/strong&gt; 。这种一次性查询的本质，使得差分法成为了处理此类构造问题的最优解，它有效规避了复杂的动态维护，将深奥的构造逻辑压缩至有限的时间复杂度之内。&lt;/p&gt;
&lt;h2&gt;构造所需的数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-increments-on-subarrays-to-form-a-target-array/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;target&lt;/code&gt; 和一个数组 &lt;code&gt;initial&lt;/code&gt; ，&lt;code&gt;initial&lt;/code&gt; 数组与 &lt;code&gt;target&lt;/code&gt; 数组有同样的大小，且一开始全部为 $0$ 。一次操作中，你可以从 &lt;code&gt;initial&lt;/code&gt; 数组中选择 &lt;strong&gt;任何&lt;/strong&gt; 子数组，并将每个值加 $1$ 。&lt;/p&gt;
&lt;p&gt;返回从 &lt;code&gt;initial&lt;/code&gt; 数组构造 &lt;code&gt;target&lt;/code&gt; 数组的最少操作次数。答案保证在 $32$ 位整数以内。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq target.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq target[i] \leq 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，其中 $N$ 表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$target_1 \quad target_2 \quad \ldots \quad target_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 2 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
3 1 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题的操作是每次可以选择一个子数组 $[l, r]$ ，并将其中所有元素同时加 $1$ 。初始数组全部为 $0$ ，目标是构造出数组 &lt;code&gt;target&lt;/code&gt; 。如果直接从区间修改的角度思考会比较复杂，因此可以通过 &lt;strong&gt;差分数组&lt;/strong&gt; 来观察数组中数值的具体变化。定义差分数组：&lt;/p&gt;
&lt;p&gt;$$
diff_i = target_i - target_{i-1}
$$&lt;/p&gt;
&lt;p&gt;差分数组反映的是数组在相邻位置之间的变化量，也就是数值发生改变的变化点。如果 &lt;code&gt;diff_i &amp;gt; 0&lt;/code&gt; ，说明在位置 $i$ 出现了上升；如果 &lt;code&gt;diff_i &amp;lt; 0&lt;/code&gt; ，说明在位置 $i$ 出现了下降。进一步考虑一次区间操作 $[l, r]$ 对差分数组的影响。区间加 $1$ 在差分数组中等价于在位置 &lt;code&gt;l&lt;/code&gt; 增加 &lt;code&gt;+1&lt;/code&gt; ，在位置 &lt;code&gt;r + 1&lt;/code&gt; 增加 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;因此，每一次操作一定会产生 &lt;strong&gt;一个正变化点和一个负变化点&lt;/strong&gt; ，因此在最终的差分数组中，一个正数变化点对应一个负数变化点。如果某一侧的数量更多，就说明有一部分变化点实际上落在数组边界之外。如果 &lt;strong&gt;正数变化量更多&lt;/strong&gt; ，说明有一部分负变化点落在数组右侧越界的位置；反之，如果 &lt;strong&gt;负数变化量更多&lt;/strong&gt; ，则说明有一部分正变化点在数组左侧越界的位置。&lt;/p&gt;
&lt;p&gt;因此，只需要统计差分数组中的正/负数变化量之和：&lt;/p&gt;
&lt;p&gt;$$
P = \sum_{i=1}^{n} \max(0, diff_i) \quad N = \sum_{i=1}^{n} \max(0, -diff_i)
$$&lt;/p&gt;
&lt;p&gt;由于这些变化点需要相互配对才能形成一次完整的区间操作，因此最少操作次数就是两者中的较大值。整个过程只需要遍历一次数组并计算差分即可完成统计，因此时间复杂度为 $O(n)$ ，空间复杂度为 $O(1)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使数列递增所需的最少操作次数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc421/tasks/abc421_g&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $n$ 的整数序列 $A = (A_1, A_2, \ldots, A_n)$ ，以及 $m$ 个整数区间 $(L_i, R_i)$ 。你可以对序列 $A$ 进行任意次数的以下操作：选择一个整数 $i$（ $1 \leq i \leq m$ ），将 $A_{L_i}, A_{L_i+1}, \ldots, A_{R_i}$ 的每一个元素加 $1$ 。&lt;/p&gt;
&lt;p&gt;请判断是否能将 $A$ 变为广义单调递增序列。若可以，求出最少需要的操作次数；若无法做到，请输出 $-1$ 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 300$&lt;/li&gt;
&lt;li&gt;$1 \leq m \leq 300$&lt;/li&gt;
&lt;li&gt;$1 \leq A_i \leq 300$&lt;/li&gt;
&lt;li&gt;$1 \leq L_i \leq R_i \leq n$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $m$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，依次表示序列 $A_1, A_2, \dots, A_n$ 。&lt;/li&gt;
&lt;li&gt;接下来 $m$ 行，每行包含两个整数 $L_i, R_i$ ，表示第 $i$ 个区间的左右边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m$&lt;/p&gt;
&lt;p&gt;$A_1 \quad A_2 \quad \ldots \quad A_n$&lt;/p&gt;
&lt;p&gt;$L_1 \quad R_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$L_m \quad R_m$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最少操作次数，若无法完成则输出 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 3
4 2 3 2
2 2
2 3
4 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
3 1 2
2 1
2 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 4
1 1 2 3
1 1
2 2
3 3
4 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;在离散的角度下看，差分数组相当于原数组的导数，因此要想让原数组单调递增，只需要让差分数组的所有元素都大于 $0$ 即可。在差分的视角下，原题中的区间加操作，本质就是将 $L_i$ 位置的数值 $+1$ ，将 $R_i + 1$ 位置的数值 $-1$ 。也就是说，每次操作相当于将 $R_i + 1$ 位置的物品分出一份给 $L_i$ 位置，并付出 $1$ 的操作代价。&lt;/p&gt;
&lt;p&gt;于是这道题就可以转化为：在若干个给定的调配通道下，能否通过最少次数的调度，将每个位置的差分值都调整到大于等于 $0$ 。这种带有运力限制与最优化代价的全局调配问题，可以转化为最小费用最大流解决。&lt;/p&gt;
&lt;p&gt;我们建立一个包含超级源点 $S$ 、超级汇点 $T$ 以及各个差分位置结点的网络拓扑。对于差分初始状态，根据各位置物资的盈缺情况进行分类连边：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 $d_i &amp;gt; 0$ 的位置，说明该处物资富余，我们从源点 $S$ 向该位置引一条流量为 $d_i$ 且费用为 $0$ 的边&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\text{Capacity}(S \to i) = d_i \quad \text{Cost}(S \to i) = 0
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 $d_i &amp;lt; 0$ 的位置，说明该处物资紧缺，我们从该位置向汇点 $T$ 引一条流量为 $-d_i$ 且费用为 $0$ 的边&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\text{Capacity}(i \to T) = -d_i \quad \text{Cost}(i \to T) = 0
$$&lt;/p&gt;
&lt;p&gt;由于原数组最后一个位置之后的差分值不需要强制大于等于 $0$ ，因此该位置相当于一个无限的物资蓄水池。我们从源点 $S$ 向该位置引一条流量为 $\infty$ 且费用为 $0$ 的边。&lt;/p&gt;
&lt;p&gt;最后，题目中给出的 $m$ 个可选区间操作，本质上就是网络中可以利用的传送通道。对于每个区间，允许将物资从 $R_i$ 无限制地运送回 $L_i - 1$ ，且每运送单位物资需要消耗 $1$ 的代价。因此，在图中建立对应的操作边：&lt;/p&gt;
&lt;p&gt;$$
\text{Capacity}(R_i \to L_i - 1) = \infty \quad \text{Cost}(R_i \to L_i - 1) = 1
$$&lt;/p&gt;
&lt;p&gt;在具体求解时，利用最大流判断可行性、最小费用计算代价的核心思想。首先统计出全网的总缺口：&lt;/p&gt;
&lt;p&gt;$$
K = \sum_{i} \max(-d_i, 0)
$$&lt;/p&gt;
&lt;p&gt;由于通往汇点 $T$ 的每条管道都受限于该位置的实际缺口大小，网络的总体最大流上限为 $K$ 。当且仅当网络的最大流精准等于 $K$ 时，才意味着每个缺水结点的管道全部达到了容量上限，即所有位置的缺口都被完整填补。此时网络流算法在达到最大流状态时所产生的总费用，即为所有缺口被补齐时所需的最小操作次数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;多重差分题目收集&lt;/h1&gt;
&lt;p&gt;在最基础的差分模型中，核心操作通常被定义为：对指定区间内的所有元素 &lt;strong&gt;同步累加单一常数项&lt;/strong&gt; 。此类操作在差分序列中具有极高的稀疏性，仅需在区间端点进行两次标量修改即可完成，逻辑结构清晰且直观。然而，在进阶的竞赛题目中，区间内的增量序列并不总是保持恒定，而是往往表现为 &lt;strong&gt;遵循特定代数规律变化的数列&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;当区间增量具有明确的函数特征（如等差序列、等比序列或低阶多项式函数）时，单一维度的差分将难以将其完全解耦。此时需引入 &lt;strong&gt;多重差分&lt;/strong&gt; 技巧：通过对原始序列执行连续阶次的差分处理，将高阶的区间变化逐步剥离，直至其退化为底层序列中的 &lt;strong&gt;常数级区间修改&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;其核心逻辑在于 &lt;strong&gt;连续差分具有降低增量数列变化规律阶数的效应&lt;/strong&gt; 。以区间累加等差数列为例，一次差分操作可将线性增长的增量转化为分段常数，而二次差分则能将该常数进一步离散化为仅在特定位置存在的数值脉冲。通过这种层级化的降阶，复杂的动态更新被压缩为极少数关键节点的数值修正，最终再通过等量的多重前缀和还原，即可实现从差分域到原始数据域的完备重构。&lt;/p&gt;
&lt;h2&gt;真寻的高效清理&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P10266&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;真寻的房间由 $n$ 行 $m$ 列的方砖组成，第 $i$ 行第 $j$ 列的方砖上初始灰尘数量为 $a_{i,j}$ 。真寻将会使用 $k$ 次清理炸弹。第 $i$ 次操作她会在第 $x_i$ 行第 $y_i$ 列的方砖上使用能量值为 $p_i$ 的清理炸弹，其清理效果如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中心点 $(x_i, y_i)$&lt;/strong&gt;：灰尘数量减少 $p_i^2$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外围第 1 圈&lt;/strong&gt;：灰尘数量减少 $(p_i - 1)^2$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外围第 2 圈&lt;/strong&gt;：灰尘数量减少 $(p_i - 2)^2$&lt;/li&gt;
&lt;li&gt;$\ldots$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外围第 $(p_i - 1)$ 圈&lt;/strong&gt;：灰尘数量减少 $1^2 = 1$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意以下两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;灰尘数量不能为负数。如果某块方砖上的灰尘数量小于要减少的量，则该方砖灰尘数量变为 $0$ 。&lt;/li&gt;
&lt;li&gt;外围第 $d$ 圈是指所有满足 $\max(|x-x_i|, |y-y_i|) = d$ 的方砖。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请输出每个方砖最终的灰尘数量。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n, m, p_i \leq 10^3$&lt;/li&gt;
&lt;li&gt;$1 \leq k \leq 10^6$&lt;/li&gt;
&lt;li&gt;$0 \leq a_{i, j} \leq 10^12$&lt;/li&gt;
&lt;li&gt;$1 \leq x_i \leq n$&lt;/li&gt;
&lt;li&gt;$1 \leq y_i \leq m$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $n$ 、$m$ 和 $k$ ，分别表示方砖行数、列数及操作次数。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行 $m$ 个整数，表示初始灰尘数组 $a_{i,j}$ 。&lt;/li&gt;
&lt;li&gt;接下来 $k$ 行，每行包含三个整数 $x_i, y_i, p_i$ ，描述一次清理操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad m \quad k$&lt;/p&gt;
&lt;p&gt;$a_{1,1} \quad a_{1,2} \quad \ldots \quad a_{1,m}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$x_1 \quad y_1 \quad p_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出 $n$ 行，每行 $m$ 个整数，表示 $k$ 次操作后每块方砖上最终的灰尘数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 5 2
7 5 4 6 5
2 4 7 9 5
6 4 5 3 5
1 2 3 0 7
2 4 2
3 3 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7 5 3 5 4 
2 3 5 4 4 
6 3 0 1 4 
1 1 2 0 7 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 7 3
6 4 7 8 4 6 1
4 5 4 6 7 5 9
1 4 3 0 7 1 3
4 6 0 7 9 0 0
1 2 3 4 4 5 8
4 7 6 8 7 4 9
5 5 3
2 3 4
3 6 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 0 0 0 0 5 1 
0 0 0 0 2 3 8 
0 0 0 0 1 0 1 
0 2 0 0 0 0 0 
0 1 1 0 0 0 7 
4 7 5 4 3 0 8 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;从题目描述来看，清理炸弹的 &lt;strong&gt;清理效果&lt;/strong&gt; 具有极强的空间对称性和数学规律。由于 &lt;strong&gt;差分数组&lt;/strong&gt; 本质上是原数组的离散导数，这种具有固定代数规律的变化量，在经过 &lt;strong&gt;高阶差分&lt;/strong&gt; 处理后，往往能被简化为极少数特征点上的变动。与其直接维护原数组并执行 $O(p^2)$ 的区域更新，不如通过维护差分数组，记录每一处数值发生改变的 &lt;strong&gt;变化点&lt;/strong&gt; ，最后利用 &lt;strong&gt;前缀和操作&lt;/strong&gt; 进行复原。&lt;/p&gt;
&lt;p&gt;如果对单次清理操作产生的增量矩阵先进行一次 &lt;strong&gt;二维差分&lt;/strong&gt;（或者分别执行一次横向和竖向的差分），我们可以观察到所得的结果表现为四条呈 &lt;strong&gt;X 型对称分布的等差数列&lt;/strong&gt; 。这意味着 &lt;strong&gt;差分的结果仍然具备规律&lt;/strong&gt; ，因此可以考虑再次进行差分。然而，这里的难点在于 &lt;strong&gt;如何选择差分方向&lt;/strong&gt; 。由于这些等差数列是沿 &lt;strong&gt;对角线&lt;/strong&gt; 分布的，如果直接使用斜向差分，虽然能简化其中一条对角线，但另一条正交方向的等差数列就没办法被简化。&lt;/p&gt;
&lt;p&gt;为了解决这个方向上的冲突，我们可以将这些对称的变化量从原图中拆分出来，分别进行维护。具体做法是准备两个 &lt;strong&gt;独立的差分辅助数组&lt;/strong&gt;：一个专门维护从 &lt;strong&gt;左上到右下&lt;/strong&gt; 方向的差分结果，另一个则维护从 &lt;strong&gt;左下到右上&lt;/strong&gt; 方向的差分结果。这样，对于每一次炸弹操作，我们只需要在两个数组对应的位置上进行标记。通过这种拆分逻辑，可以将原本互相干扰的两个方向解耦，使它们在各自的差分维度下都能被简化。&lt;/p&gt;
&lt;p&gt;在最终复原时，我们首先沿着各自对应的斜向方向进行 &lt;strong&gt;第二次前缀和复原&lt;/strong&gt; ，将差分标记还原为等差数列。随后，将这两个数组的结果进行 &lt;strong&gt;叠加&lt;/strong&gt; ，再统一执行一次 &lt;strong&gt;二维前缀和处理&lt;/strong&gt;（或者分别进行一次横向和竖向的前缀和），从而得到每个位置累计的总清理量 $S_{i,j}$ 。最终方砖的灰尘残余量通过以下公式计算：&lt;/p&gt;
&lt;p&gt;$$
\text{result}&lt;em&gt;{i,j} = \max(0, a&lt;/em&gt;{i,j} - S_{i,j})
$$&lt;/p&gt;
&lt;p&gt;这种方法巧妙地利用了 &lt;strong&gt;斜向差分&lt;/strong&gt; 对对角线等差序列的压缩能力，通过将不同方向的规律 &lt;strong&gt;拆分再叠加&lt;/strong&gt; ，成功绕开了直接在二维平面上处理复杂增量的难题。整个过程只需要在标记时花费 $O(k)$ 的时间，并在最后通过 $O(n \cdot m)$ 的扫描完成复原，在 &lt;strong&gt;时间复杂度&lt;/strong&gt; 与 &lt;strong&gt;逻辑严密性&lt;/strong&gt; 上都达到了很好的平衡。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;有趣的组合数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/CF407C&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;杨辉三角组合数学差分&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;等式条件变为不等条件相关题目收集&lt;/h1&gt;
&lt;p&gt;在许多复杂的计数与组合问题中，&lt;strong&gt;等式条件往往显得过于严苛且缺乏单调性&lt;/strong&gt; ，直接对其进行状态定义或转移通常会面临极高的思维难度。相比之下，&lt;strong&gt;不等式的限制条件则更为宽松&lt;/strong&gt; ，且往往具备天然的单调特性，这使得我们可以利用双指针、二分查找或动态规划等手段更高效地处理。因此，我们经常采用一种类似差分的转换思路，将原本难以企及的精确等式条件，拆解为两个容易统计的不等式之差：&lt;/p&gt;
&lt;p&gt;$$
\text{count}(\text{ans} = k) = \text{count}(\text{ans} \leq k) - \text{count}(\text{ans} \leq k-1)
$$&lt;/p&gt;
&lt;p&gt;这种技巧的核心逻辑在于，我们不再死磕 &lt;strong&gt;恰好等于某个数&lt;/strong&gt; 这个严格的条件，而是去统计 &lt;strong&gt;不超过某个数&lt;/strong&gt; 这个较为宽松的条件，通过放宽约束条件来换取计算上的便利性。由于不等式计数通常允许我们使用滑动窗口等具备单调性的算法，往往能将原本 $O(n^2)$ 甚至更高复杂度的做法强行简化。&lt;/p&gt;
&lt;p&gt;事实上，这种从精确判定向范围统计过渡的策略，其本质是对计数维度 &lt;strong&gt;进行前缀化处理&lt;/strong&gt; 。这一思想在组合数学中具有深远的意义，&lt;strong&gt;二项式反演&lt;/strong&gt; 便是该逻辑的高阶体现。二项式反演通过建立 “至多/至少” 与 “恰好” 之间的代数映射，利用广义容斥原理将难以直接刻画的属性分布转化为累积量的组合叠加。尽管其底层数学结构更为精精密，但其核心诉求始终在于通过放宽约束来换取统计上的可行性。&lt;/p&gt;
&lt;h3&gt;数学期望的尾概率公式&lt;/h3&gt;
&lt;p&gt;在求解数学期望时，我们常常会遇到因状态过于复杂而难以刻画的情况。为了打破传统求解方法带来的计算僵局，算法竞赛中通常会引入一种 &lt;strong&gt;将点概率转化为范围概率&lt;/strong&gt; 的思维模型，通过 &lt;strong&gt;重新构建期望的计算维度&lt;/strong&gt; 来简化逻辑。&lt;/p&gt;
&lt;p&gt;中学阶段经常会遇到求解数学期望的问题，我们常用的期望求解公式是：&lt;/p&gt;
&lt;p&gt;$$
E[X] = \sum_{i=1}^{\infty} i \cdot P(X = i) = 1 \cdot P(X=1) + 2 \cdot P(X=2) + 3 \cdot P(X=3)
$$&lt;/p&gt;
&lt;p&gt;而竞赛当中我们经常使用期望的 &lt;strong&gt;尾概率公式&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
E[X] = \sum_{i=1}^{\infty} P(X \geq i) = P(X \geq 1) + P(X \geq 2) + P(X \geq 3)
$$&lt;/p&gt;
&lt;p&gt;原因是中学阶段的公式中包含的 $P(X = i)$ 求解的是 “恰好等于” 的概率，在算法竞赛中通常难以维护，因此一般使用包含 $P(X \geq i)$ 的尾概率公式。而这个公式的推导非常简单，只需要用到 &lt;strong&gt;初中的因式分解&lt;/strong&gt; 即可。&lt;/p&gt;
&lt;p&gt;我们可以将公式排列为一个 &lt;strong&gt;三角形矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
E[X] = \quad &amp;amp; P(X=1) \&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\quad &amp;amp; P(X=2) + P(X=2) \&lt;/li&gt;
&lt;li&gt;\quad &amp;amp; P(X=3) + P(X=3) + P(X=3)
\end{aligned}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统的求解方式是 &lt;strong&gt;横向求和&lt;/strong&gt; ，即先算出每一行的结果再相加。现在我们 &lt;strong&gt;改变视角&lt;/strong&gt; ，将这个矩阵 &lt;strong&gt;纵向按列合并&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一列包含了所有可能发生的事件概率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(X = 1) + P(X = 2) + P(X = 3) = P(X \geq 1)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二列去掉了 $X = 1$ 的情况，包含了所有大于等于 $2$ 的事件概率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(X=2) + P(X=3) = P(X \geq 2)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第三列去掉了 $X = 1, 2$ 的情况，包含了所有大于等于 $3$ 的事件概率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(X = 3) = P(X \geq 3)
$$&lt;/p&gt;
&lt;p&gt;不难发现，将所有纵列的结果相加，其总和依然与原式完全相等。这种通过 &lt;strong&gt;改变求和顺序&lt;/strong&gt; 的手法，在统计学中相当于把原本横向切割的概率条改为了 &lt;strong&gt;纵向叠加的后缀和&lt;/strong&gt; 。它巧妙地将互相制约的点概率问题转化为了 &lt;strong&gt;具备优美单调性的范围统计&lt;/strong&gt; ，从而极大地降低了复杂极值问题的状态定义与转移难度。&lt;/p&gt;
&lt;h2&gt;K种整数子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subarrays-with-k-different-integers/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个正整数数组 $nums$ 和一个整数 $k$ ，返回 $nums$ 中 &lt;strong&gt;「好子数组」&lt;/strong&gt; 的数目。如果 $nums$ 的某个子数组中不同整数的个数恰好为 $k$ ，则称 $nums$ 的这个连续、不一定不同的子数组为「好子数组」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子数组&lt;/strong&gt; 是数组的 &lt;strong&gt;连续&lt;/strong&gt; 部分。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 2 * 10^4$&lt;/li&gt;
&lt;li&gt;$1 \leq nums[i], k \leq nums.length$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示好子数组的数目。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 2
1 2 1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 3
1 2 1 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;在处理子数组计数问题时，一个自然的尝试是使用 &lt;strong&gt;滑动窗口&lt;/strong&gt; 技巧。然而，滑动窗口的适用性严格受限于问题的 &lt;strong&gt;单调性&lt;/strong&gt; 。只有当窗口状态随边界移动呈现出 “越长越满足” 或 “越短越满足” 的趋势时，窗口的扩张与收缩才有据可依。通常情况下，这类具备单调性的约束表现为 &lt;strong&gt;“至少 k 种”&lt;/strong&gt; 或 &lt;strong&gt;“至多 k 种”&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在本题中，核心目标是统计 &lt;strong&gt;“恰好有 k 个不同整数”&lt;/strong&gt; 的子数组。由于 “恰好” 这一等式条件本身不具备单调性，窗口的任何移动都可能导致状态在合法与非法之间随机切换，因此无法直接利用滑动窗口进行稳定维护。这种维护性的缺失，正是该类题目难以直接求解的根源。&lt;/p&gt;
&lt;p&gt;为了破解这一困局，我们可以借助上面提到过的 &lt;strong&gt;广义差分思路&lt;/strong&gt; ，将严苛的等式约束差分为具备单调性的不等式条件。通过将恰好型计数转化为两个 &lt;strong&gt;逻辑一致仅参数不同&lt;/strong&gt; 的不等式计数：&lt;/p&gt;
&lt;p&gt;$$
\text{count}(\text{distinct} = k) = \text{count}(\text{distinct} \le k) - \text{count}(\text{distinct} \le k-1)
$$&lt;/p&gt;
&lt;p&gt;在这种转化下，原命题被拆解为对 &lt;strong&gt;同一种不等式判定逻辑&lt;/strong&gt; 的两次调用。由于 “至多 $k$ 个不同整数” 这一判定条件具有完美的单调性，我们可以通过复用同一套滑动窗口算法逻辑，分别计算出两个累积区间对应的子数组数量，再通过作差间接推导出 &lt;strong&gt;“恰好 k 个不同整数”&lt;/strong&gt; 的精确结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 2e4 + 100;
int N, k;
int nums[MAXN];

int count(int k, int* a){
    int left = -1, pre = 0, ans = 0;
    unordered_map&amp;lt;int, int&amp;gt; counts;
    for (int right = 0; right &amp;lt; N; right ++){
        counts[a[right]] += 1;
        while (left &amp;lt;= right &amp;amp;&amp;amp; (int)counts.size() &amp;gt; k){
            counts[a[++left]]--;
            if (!counts[a[left]])
                counts.erase(a[left]);
        }
        ans += right - left;
    }

    return ans;
}

int main() {
    cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; k;
    for (int i = 0; i &amp;lt; N; i++){
        cin &amp;gt;&amp;gt; nums[i];
    }

    cout &amp;lt;&amp;lt; count(k, nums) - count(k - 1, nums);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最值的数学期望&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc411/tasks/abc411_e&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;有 $n$ 个六面骰子，编号为 $1 \sim n$ 。骰子 $i$ 的六个面上分别写着数值 $A_{i,1}, A_{i,2}, \ldots, A_{i,6}$ 。&lt;/p&gt;
&lt;p&gt;现在将这 $n$ 个骰子同时掷出。请计算所有骰子朝上面数值的 &lt;strong&gt;最大值&lt;/strong&gt; 的数学期望，结果对 $998244353$ 取模。&lt;/p&gt;
&lt;p&gt;每个骰子的面是独立且等概率出现的。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq A_{i,j} \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ 。&lt;/li&gt;
&lt;li&gt;接下来 $n$ 行，每行包含 $6$ 个整数，依次表示第 $i$ 个骰子的面数值。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$A_{1,1} \quad A_{1,2} \quad \ldots \quad A_{1,6}$&lt;/p&gt;
&lt;p&gt;$A_{2,1} \quad A_{2,2} \quad \ldots \quad A_{2,6}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$A_{n,1} \quad A_{n,2} \quad \ldots \quad A_{n,6}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示最大值的数学期望。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
1 1 4 4 4 4
1 1 1 3 3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;332748121
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
1 1 1 1 1 1
2 2 2 2 2 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8
55 76 80 21 34 28
82 84 2 32 56 17
11 57 37 28 39 18
47 2 97 25 75 29
72 45 22 75 26 81
6 79 16 68 68 40
31 80 68 57 18 55
49 10 63 91 93 40
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;213725517
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;尾概率公式&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;区间求和变为前缀求和相关题目收集&lt;/h1&gt;
&lt;p&gt;在许多的算法问题中，&lt;strong&gt;直接处理区间求和往往不够直观&lt;/strong&gt; ，不仅实现复杂，还容易在枚举区间的过程中产生大量重复的计算；而当我们将问题转化为 &lt;strong&gt;前缀求和形式&lt;/strong&gt; 后，整体结构通常会变得更加清晰，计算过程也更加高效。因此，在处理涉及区间统计的问题时，一个非常常见的思路就是 &lt;strong&gt;先将区间求和转写为前缀和之间的差值关系&lt;/strong&gt; 。通过引入前缀和数组，我们可以把所有区间约束统一表示为端点之间的关系，从而简化整体的推导过程：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i = l}^{r} a_i = pre[r] - pre[l-1]
$$&lt;/p&gt;
&lt;p&gt;这种技巧的核心在于，我们可以用 &lt;strong&gt;全局累积信息代替局部区间计算&lt;/strong&gt; 。通过预处理前缀和数组，我们能够在 $O(1)$ 的时间内得到任意区间的和，使原本需要枚举整段区间的操作被压缩为一次简单的差值计算。这样不仅可以显著降低时间复杂度，也能够让很多原本分散的区间操作被统一到同一种表达框架下，从而方便后续的建模与优化。&lt;/p&gt;
&lt;p&gt;在完成区间到前缀的转化之后，问题往往会进一步转化为 &lt;strong&gt;两个前缀值之间的关系问题&lt;/strong&gt; 。这时我们就可以借助类似&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-note/two-sum-idea/two-sum-idea/&quot;&gt;两数之和&lt;/a&gt;的思想来进行处理：&lt;strong&gt;固定右端点的信息，动态维护左端点可能出现的前缀值&lt;/strong&gt; 。在一些题目中，这种维护过程可以通过 &lt;strong&gt;双指针&lt;/strong&gt; 来实现，例如保持右端点单调移动，同时用左指针维护满足条件的范围；而在另一些问题中，也可能需要借助 &lt;strong&gt;哈希表&lt;/strong&gt; 等数据结构来统计满足条件的前缀值数量。&lt;/p&gt;
&lt;p&gt;从更宏观的角度来看，将区间求和转化为前缀和之差本质上是一种 &lt;strong&gt;降维过程&lt;/strong&gt; 。它将涉及多个变量的区间动态累加，成功映射为仅涉及两个前缀值的简单运算。这种转化在不改变问题原本的数学性质的前提下，极大地精简了统计规模。由于前缀和之差在逻辑上完全等价于区间累加和，&lt;strong&gt;将区间和转换为前缀和几乎是处理此类问题的必然思路&lt;/strong&gt; 。任何能直接在原始问题上生效的解法，在转化后的框架下不仅完全兼容，且往往能展现出更优的处理效率。&lt;/p&gt;
&lt;h2&gt;数组的递增划分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/ways-to-split-array-into-three-subarrays/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;我们称一个分割整数数组的方案是 &lt;strong&gt;好的&lt;/strong&gt; ，当它满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组被分成三个 &lt;strong&gt;非空&lt;/strong&gt; 连续子数组，从左至右分别命名为 &lt;code&gt;left&lt;/code&gt; ，&lt;code&gt;mid&lt;/code&gt; ，&lt;code&gt;right&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;left&lt;/code&gt; 中元素和小于等于 &lt;code&gt;mid&lt;/code&gt; 中元素和，&lt;code&gt;mid&lt;/code&gt; 中元素和小于等于 &lt;code&gt;right&lt;/code&gt; 中元素和。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给你一个 &lt;strong&gt;非负&lt;/strong&gt; 整数数组 &lt;code&gt;nums&lt;/code&gt; ，请你返回 &lt;strong&gt;好的&lt;/strong&gt; 分割 &lt;code&gt;nums&lt;/code&gt; 方案数目。&lt;/p&gt;
&lt;p&gt;由于答案可能会很大，请你将结果对 $10^9 + 7$ 取余后返回。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$3 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^4$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
1 2 2 2 5 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题要求把数组 &lt;code&gt;nums&lt;/code&gt; 分割成三个 &lt;strong&gt;非空连续子数组&lt;/strong&gt; &lt;code&gt;left&lt;/code&gt; 、&lt;code&gt;mid&lt;/code&gt; 、&lt;code&gt;right&lt;/code&gt;，并满足两个条件：&lt;code&gt;sum(left) ≤ sum(mid)&lt;/code&gt; 且 &lt;code&gt;sum(mid) ≤ sum(right)&lt;/code&gt; 。由于需要频繁比较区间和，如果每次重新计算会非常低效，因此可以先构建 &lt;strong&gt;前缀和数组&lt;/strong&gt; 来简化计算。设 &lt;code&gt;pre[i]&lt;/code&gt; 表示数组前 $i$ 个元素的和，那么整个数组的总和就是 &lt;code&gt;pre[n]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;当数组在位置 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;mid&lt;/code&gt; 处分割时，三个部分的和可以用前缀和直接表示：&lt;code&gt;left&lt;/code&gt; 的和为 &lt;code&gt;pre[left]&lt;/code&gt; ，&lt;code&gt;mid&lt;/code&gt; 的和为 &lt;code&gt;pre[mid] - pre[left]&lt;/code&gt; ，&lt;code&gt;right&lt;/code&gt; 的和为 &lt;code&gt;pre[n] - pre[mid]&lt;/code&gt; 。这样一来，只需要把题目中的条件转化为前缀和之间的不等式即可。&lt;/p&gt;
&lt;p&gt;首先考虑条件 &lt;code&gt;sum(left) ≤ sum(mid)&lt;/code&gt; 。代入前缀和表达式可以得到：&lt;/p&gt;
&lt;p&gt;$$
pre[left] \leq pre[mid] - pre[left]
$$&lt;/p&gt;
&lt;p&gt;整理后得到：&lt;/p&gt;
&lt;p&gt;$$
2 \times pre[left] \leq pre[mid]
$$&lt;/p&gt;
&lt;p&gt;这说明当 &lt;code&gt;left&lt;/code&gt; 固定时，&lt;code&gt;mid&lt;/code&gt; 的前缀和至少需要达到 &lt;code&gt;2 * pre[left]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;接下来考虑第二个条件 &lt;code&gt;sum(mid) ≤ sum(right)&lt;/code&gt; 。同样代入前缀和表达式可以得到：&lt;/p&gt;
&lt;p&gt;$$
pre[mid] - pre[left] \leq pre[n] - pre[mid]
$$&lt;/p&gt;
&lt;p&gt;整理后得到：&lt;/p&gt;
&lt;p&gt;$$
2 \times pre[mid] \leq pre[left] + pre[n]
$$&lt;/p&gt;
&lt;p&gt;进一步可以写成：&lt;/p&gt;
&lt;p&gt;$$
pre[mid] \leq \frac{pre[left] + pre[n]}{2}
$$&lt;/p&gt;
&lt;p&gt;因此在 &lt;code&gt;left&lt;/code&gt; 固定时，&lt;code&gt;mid&lt;/code&gt; 的前缀和必须满足如下范围：&lt;/p&gt;
&lt;p&gt;$$
2 \times pre[left] \leq pre[mid] \leq \frac{pre[left] + pre[n]}{2}
$$&lt;/p&gt;
&lt;p&gt;接下来可以得到一个非常重要的 &lt;strong&gt;剪枝条件&lt;/strong&gt; 。因为 &lt;code&gt;mid&lt;/code&gt; 的最小值是 &lt;code&gt;2 * pre[left]&lt;/code&gt; ，如果这个值已经大于右侧上界 &lt;code&gt;(pre[left] + pre[n]) / 2&lt;/code&gt; ，那么就不存在合法的 &lt;code&gt;mid&lt;/code&gt; 。将不等式整理后可以得到：&lt;/p&gt;
&lt;p&gt;$$
3 \times pre[left] \leq pre[n]
$$&lt;/p&gt;
&lt;p&gt;也就是说，当 &lt;code&gt;pre[left]&lt;/code&gt; 满足下面条件：&lt;/p&gt;
&lt;p&gt;$$
pre[left] &amp;gt; \frac{pre[n]}{3}
$$&lt;/p&gt;
&lt;p&gt;无论怎样选择 &lt;code&gt;mid&lt;/code&gt; 都无法满足条件，因此可以直接停止枚举 &lt;code&gt;left&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在算法实现时，可以先计算前缀和数组，然后枚举 &lt;code&gt;left&lt;/code&gt; 的位置。当 &lt;code&gt;left&lt;/code&gt; 固定后，&lt;code&gt;mid&lt;/code&gt; 的取值范围实际上对应前缀和数组中一段连续的区间：下界是第一个满足 &lt;code&gt;pre[mid] ≥ 2 * pre[left]&lt;/code&gt; 的位置，而上界是最后一个满足 &lt;code&gt;pre[mid] ≤ (pre[left] + pre[n]) / 2&lt;/code&gt; 的位置。由于前缀和数组是单调递增的，因此可以使用 &lt;strong&gt;二分查找&lt;/strong&gt; 来快速定位这两个边界。最后题目要求结果对 $10^9 + 7$ 进行取模。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指定区间累加和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc404/tasks/abc404_g&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数 $N$ 和长度为 $M$ 的整数序列：&lt;/p&gt;
&lt;p&gt;$$
L = (L_1, L_2, \ldots, L_M) \quad
R = (R_1, R_2, \ldots, R_M) \quad
S = (S_1, S_2, \ldots, S_M)
$$&lt;/p&gt;
&lt;p&gt;确定是否存在一个长度为 $N$ 的正整数序列 $A$ 满足以下条件：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=L_i}^{R_i} A_j = S_i (1 \leq i \leq M)
$$&lt;/p&gt;
&lt;p&gt;如果存在这样的序列，找到 $A$ 的最小可能和。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N, M \leq 4000$&lt;/li&gt;
&lt;li&gt;$1 \leq L_i \leq R_i \leq N$&lt;/li&gt;
&lt;li&gt;$1 \leq S_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $M$ 。&lt;/li&gt;
&lt;li&gt;接下来的 $M$ 行，每行包含三个整数 $L$ 、$R$ 和 $S$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad M$&lt;/p&gt;
&lt;p&gt;$L_1 \quad R_1 \quad S_1$&lt;/p&gt;
&lt;p&gt;$L_2 \quad R_2 \quad S_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$L_M \quad R_M \quad S_M$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;如果不存在满足条件且长度为 $N$ 的正整数序列 $A$ ，则输出 &lt;code&gt;-1&lt;/code&gt; ；否则，输出 $A$ 的最小可能总和。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 3
1 2 4
2 3 5
5 5 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;12
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2
1 1 1
1 1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9 6
8 9 8
3 6 18
2 4 19
5 6 8
3 5 14
1 3 26
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;44
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;差分约束&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;双边条件变为单边条件相关题目收集&lt;/h1&gt;
&lt;p&gt;在算法竞赛中，我们经常会遇到需要统计满足某种 &lt;strong&gt;双边约束&lt;/strong&gt; 的组合数量、子数组个数或元素分布的问题。这类问题的核心矛盾通常体现在条件的双向限制上，即要求某个统计量 $T$ 必须同时受限于给定的上下界：&lt;/p&gt;
&lt;p&gt;$$
\text{lower} \leq T \leq \text{upper}
$$&lt;/p&gt;
&lt;p&gt;若直接对所有情况进行合法性检测，往往会面临极高的算法复杂度。解决此类问题的关键在于将原本的 &lt;strong&gt;双边不等式约束&lt;/strong&gt; 转化为更容易处理的 &lt;strong&gt;单边不等式逻辑&lt;/strong&gt; 。原本的条件 $\text{lower} \leq T \leq \text{upper}$ 可以等价地解构为两个单边条件的交集，即 $T \leq \text{upper}$ 与 $T \geq \text{lower}$ 。在此基础上，通过引入计数函数 $f(X)$ 来表示满足单边约束 $T \leq X$ 的样本总量，原命题即可转化为标准的差分形式：&lt;/p&gt;
&lt;p&gt;$$
\text{count}(\text{lower} \leq T \leq \text{upper}) = f(\text{upper}) - f(\text{lower} - 1)
$$&lt;/p&gt;
&lt;p&gt;这种转化的核心逻辑在于：&lt;strong&gt;通过主动放宽约束边界，将双向限制问题降维成易于维护的单边单调性问题&lt;/strong&gt; 。在这种视角下，我们不再受困于双向边界的同步校验，而是通过解决约束宽松且具有前缀性质的单边统计问题，并利用差分技巧还原出精确的双边计数结果。这种以退为进的逻辑转化，能有效利用单向条件的单调性来适配双指针或二分等高效算法，将棘手的双边判定条件，重新包装为简单的单边判定条件。&lt;/p&gt;
&lt;p&gt;值得一提的是，这类双边约束条件天然契合&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/dp-classification/digit-dp/&quot;&gt;数位 DP&lt;/a&gt;相关问题。例如统计某个区间内满足特定条件的数字，原本的双边约束可以直接转化为单边约束的计数函数 $f(X)$ 。然而数位 DP 中出现的上下界往往非常大，如果直接计算 $f(\text{lower}-1)$ 可能涉及高精度计算。为了避免这种情况，我们可以先计算 $f(\text{upper}) - f(\text{lower})$ ，再单独判断 $lower$ 是否满足要求。&lt;/p&gt;
&lt;h2&gt;统计公平的数对&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-the-number-of-fair-pairs/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个下标从 $0$ 开始、长度为 $n$ 的整数数组 &lt;code&gt;nums&lt;/code&gt; ，和两个整数 &lt;code&gt;lower&lt;/code&gt; 和 &lt;code&gt;upper&lt;/code&gt; ，返回 &lt;strong&gt;公平数对的数目&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果 $(i, j)$ 数对满足以下情况，则认为它是一个 &lt;strong&gt;公平数对&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$0 &amp;lt;= i &amp;lt; j &amp;lt; n$&lt;/li&gt;
&lt;li&gt;$lower &amp;lt;= nums[i] + nums[j] &amp;lt;= upper$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$nums.length == n$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq lower \leq upper \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含三个整数 $N$ 、$lower$ 和 $upper$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad lower \quad upper$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 3 6
0 1 7 4 4 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 11 11
1 7 9 2 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题要求统计满足 $i &amp;lt; j$ 且 &lt;code&gt;lower ≤ nums[i] + nums[j] ≤ upper&lt;/code&gt; 的数对数量。直接枚举所有 $(i, j)$ 的时间复杂度是 $O(n^2)$ ，显然无法通过 $10^5$ 的数据规模，因此需要换一种思路。&lt;/p&gt;
&lt;p&gt;一个常见的技巧是把区间计数问题转化为 &lt;strong&gt;两个前缀计数之差&lt;/strong&gt; 。设 &lt;code&gt;count(x)&lt;/code&gt; 表示满足 &lt;code&gt;nums[i] + nums[j] ≤ x&lt;/code&gt; 的数对数量，那么题目要求的答案就可以表示为：&lt;/p&gt;
&lt;p&gt;$$
count(upper) - count(lower - 1)
$$&lt;/p&gt;
&lt;p&gt;因此问题就转化为了如何高效计算 &lt;code&gt;count(x)&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;为了计算 &lt;code&gt;count(x)&lt;/code&gt; ，可以先对数组进行排序。排序之后，如果固定左端点 $i$ ，随着 $j$ 的增大，&lt;code&gt;nums[i] + nums[j]&lt;/code&gt; 也会单调增加，因此可以使用 &lt;strong&gt;双指针&lt;/strong&gt; 来统计合法的数对数量。&lt;/p&gt;
&lt;p&gt;通过这样的方式就可以在线性时间内计算出 &lt;code&gt;count(x)&lt;/code&gt; 。最后分别计算 &lt;code&gt;count(upper)&lt;/code&gt; 和 &lt;code&gt;count(lower - 1)&lt;/code&gt; ，两者相减即可得到最终答案。整个算法的时间复杂度为 $O(n \log n)$ ，双指针统计部分为 $O(n)$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/basic/prefix-sum/&quot;&gt;【OI WiKi】前缀和与差分相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/myRealization/article/details/104594255&quot;&gt;【CSDN 博客】算法技巧之差分&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【博客指南】如何创建一个独立页面</title><link>https://xingguang641.com/posts/blog/blog-guide/static-pages/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/static-pages/</guid><description>基于 Fuwari 模板创建自定义独立页面（如成就、友链等）的完整教程</description><pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本文将介绍如何在 Fuwari 模板中添加一个自定义的独立页面（例如 “成就” 、“友链” 等）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;编写页面内容&lt;/h2&gt;
&lt;p&gt;首先，我们需要准备该独立页面所要展示的主体内容。&lt;/p&gt;
&lt;p&gt;请在 &lt;code&gt;src/content/spec&lt;/code&gt; 目录下创建一个 Markdown 文件（例如 &lt;code&gt;achievements.md&lt;/code&gt; ）。这个文件不需要包含复杂的 Frontmatter，直接编写你想要展示的 Markdown 内容即可。&lt;/p&gt;
&lt;h2&gt;创建页面组件&lt;/h2&gt;
&lt;p&gt;接下来，我们需要创建一个 Astro 页面文件来渲染上述内容。在 &lt;code&gt;src/pages&lt;/code&gt; 目录下新建一个 &lt;code&gt;.astro&lt;/code&gt; 文件（推荐与你的 Markdown 文件名一致，例如 &lt;code&gt;achievements.astro&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;你可以直接复制 &lt;code&gt;about.astro&lt;/code&gt; 的样例代码并稍作修改，或者使用下面的自定义模板代码。该代码会自动获取 &lt;code&gt;src/content/spec&lt;/code&gt; 中的内容并渲染到主布局中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import { getEntry, render } from &quot;astro:content&quot;;
import Markdown from &quot;@components/misc/Markdown.astro&quot;;
import MainGridLayout from &quot;../layouts/MainGridLayout.astro&quot;;

// 获取步骤 1 中创建的 &apos;achievements&apos; 内容
// 如果你的文件名是 other.md，请将下方的 &quot;achievements&quot; 改为 &quot;other&quot;
const achievementsPost = await getEntry(&quot;spec&quot;, &quot;achievements&quot;);

if (!achievementsPost) {
	throw new Error(&quot;Achievements page content not found&quot;);
}

const { Content } = await render(achievementsPost);
---
&amp;lt;!-- title 和 description 将显示在浏览器标签页和 SEO 信息中 --&amp;gt;
&amp;lt;MainGridLayout title=&quot;成就&quot; description=&quot;我的个人成就清单&quot;&amp;gt;
    &amp;lt;div class=&quot;flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32&quot;&amp;gt;
        &amp;lt;div class=&quot;card-base z-10 px-9 py-6 relative w-full &quot;&amp;gt;
            &amp;lt;Markdown class=&quot;mt-2&quot;&amp;gt;
                &amp;lt;Content /&amp;gt;
            &amp;lt;/Markdown&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置页面导航&lt;/h2&gt;
&lt;p&gt;页面文件准备好后，最后一步是将其添加至博客顶部导航栏，使访客能够正常访问。&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;src/config.ts&lt;/code&gt; 文件，找到常量 &lt;code&gt;navBarConfig&lt;/code&gt; ，并在 &lt;code&gt;links&lt;/code&gt; 数组中添加一项新的导航链接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const navBarConfig: NavBarConfig = {
    links: [
        LinkPreset.Home,
        LinkPreset.Archive,
        LinkPreset.About,
        // 添加新的页面入口
        // name: 导航栏显示的文字
        // url: 对应的路由地址（即 src/pages/ 下的文件名）
        { name: &quot;成就&quot;, url: &quot;/achievements/&quot; },
    ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存所有文件后，你就能在博客的顶部导航栏看到新添加的独立页面了。&lt;/p&gt;
</content:encoded></item><item><title>【深度学习基本模型】第一节：神经网络</title><link>https://xingguang641.com/posts/neural-network/neural-network/</link><guid isPermaLink="true">https://xingguang641.com/posts/neural-network/neural-network/</guid><description>介绍深度学习常见的模型</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;神经网络基本结构&lt;/h1&gt;
&lt;p&gt;在机器学习的发展历程中，人们始终试图构建一种能够 &lt;strong&gt;自动从数据中提取并表达复杂规律&lt;/strong&gt; 的模型。传统方法往往依赖人工设计的特征或受限的函数族，而在高维数据、复杂结构或大规模应用场景下，这些方法通常难以胜任。&lt;strong&gt;神经网络（Neural Networks）&lt;/strong&gt; 的出现，为这一难题提供了一套统一而灵活的解决方案。它以高度参数化的结构为基础，能够在同一框架下处理图像、语音、文本等多种类型的数据，成为现代机器学习的重要支柱。&lt;/p&gt;
&lt;p&gt;神经网络的思想最早来源于对生物神经系统的抽象。1940–1950 年代，McCulloch–Pitts 提出了最初的神经元逻辑模型，为这一研究方向奠定了数学基础。随后 Rosenblatt 提出的 &lt;strong&gt;感知机（Perceptron）&lt;/strong&gt; 展示了利用简单神经元构建可学习分类器的可能性。进入 1980 年代，多层神经网络结构的提出以及反向传播算法（Backpropagation）的成功应用，使研究者得以训练更深层的网络；再加上数据规模与计算硬件的快速提升，神经网络逐渐发展为现代人工智能的核心方法之一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cneural-network%5C%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C1.png&quot; alt=&quot;神经网络图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;随着理论体系的完善，人们逐渐认识到神经网络不仅是受生物结构启发的计算模型，更可以从 &lt;strong&gt;函数逼近（function approximation）&lt;/strong&gt; 与 &lt;strong&gt;统计学习理论（statistical learning theory）&lt;/strong&gt; 的角度进行系统研究。经典的万能逼近定理说明，只要网络的结构足够宽或足够深，前馈神经网络便可以逼近几乎任意连续函数；而现代泛化理论也表明，即使参数维度极高，适当的结构选择、训练方式与正则化策略仍能够使模型具备良好的泛化能力。&lt;/p&gt;
&lt;p&gt;在应用层面，神经网络已经形成覆盖广泛任务的一整套技术体系。卷积神经网络成为图像处理和视觉任务中的标准模型；循环网络与后来的注意力机制推动了自然语言处理的范式转变；而多层感知机则因其结构简单、适用性强，持续被用于各类预测与决策模块中。无论是在监督学习、生成模型还是强化学习中，神经网络都展现出强大的表达能力与适配性。&lt;/p&gt;
&lt;p&gt;整体而言，神经网络从最初的生物启发式模型逐步演化为现代深度学习的核心基础设施。凭借其高表达力、可扩展性与普适建模能力，它已经成为理解当代人工智能体系不可或缺的一部分。&lt;/p&gt;
&lt;h2&gt;激活函数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;激活函数&lt;/strong&gt; 是神经网络中施加于神经元输出的非线性映射，用以调节输入信号在各层之间的传递方式。它决定了网络的局部响应结构，使每一层能够在不同输入区域呈现出截然不同的变换行为。通过这种可控的非线性，激活函数为模型提供了构建复杂映射所需的基础操作单元。&lt;/p&gt;
&lt;p&gt;不同激活函数在光滑性、是否存在饱和区间、梯度幅度、数值稳定性以及计算代价等方面具有显著差异。这些性质会直接影响模型的梯度传播效率、参数更新动态以及最终的收敛表现。因此，激活函数不仅是神经网络结构的基本组成模块，更是训练效果与泛化能力的重要调控手段，需要根据具体任务与网络结构进行合理选择。&lt;/p&gt;
&lt;h3&gt;非线性层的必要性&lt;/h3&gt;
&lt;p&gt;在神经网络中引入非线性变换是提升模型表达能力的关键步骤。若网络仅由线性运算组成，即使堆叠任意多层，其整体仍可等价地被合并为一次线性映射，从而无法刻画真实任务中普遍存在的复杂 “输入–输出” 关系。非线性层的加入使网络能够构造更丰富的函数族，使模型具备对高维结构、局部变化及复杂决策边界进行拟合的能力，是现代深度学习模型得以成功的基础。&lt;/p&gt;
&lt;p&gt;我们首先考虑最基本的前馈结构。设输入向量 $x \in \mathbb{R}^d$ ，第 1 层与第 2 层均为仿射变换：&lt;/p&gt;
&lt;p&gt;$$
h = W_1 x + b_1 \qquad y = W_2 h + b_2
$$&lt;/p&gt;
&lt;p&gt;将两层合并可得：&lt;/p&gt;
&lt;p&gt;$$
y = W_2 (W_1 x + b_1) + b_2
= (W_2 W_1) x + (W_2 b_1 + b_2)
$$&lt;/p&gt;
&lt;p&gt;即使网络包含更多层，只要每层均为线性（或仿射）变换：&lt;/p&gt;
&lt;p&gt;$$
h^{(k)} = W_k h^{(k-1)} + b_k
$$&lt;/p&gt;
&lt;p&gt;其整体仍可折叠为单一映射：&lt;/p&gt;
&lt;p&gt;$$
h^{(L)} = W_{\mathrm{eff}} x + b_{\mathrm{eff}}
$$&lt;/p&gt;
&lt;p&gt;因此，一个完全由线性层堆叠而成的前馈网络，其函数族始终为：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{F}_\text{linear}
= { x \mapsto Wx + b \mid W, b \text{ 任意} }
$$&lt;/p&gt;
&lt;p&gt;无法逼近绝大多数非线性函数族（如 XOR、分段复杂决策边界、图像—语言映射等）。这一限制直接导致模型无法处理现代机器学习中常见的高复杂度任务。&lt;/p&gt;
&lt;p&gt;在网络中引入非线性函数 $\sigma(\cdot)$ 后，每层结构变为：&lt;/p&gt;
&lt;p&gt;$$
h^{(k)} = \sigma(W_k h^{(k-1)} + b_k)
$$&lt;/p&gt;
&lt;p&gt;最终得到的函数族形式为：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{F}&lt;em&gt;{\mathrm{NN}}
= \Big{
x \mapsto
W_L, \sigma!\Big( W&lt;/em&gt;{L-1}, \sigma(\cdots \sigma(W_1 x + b_1)\cdots) + b_{L-1} \Big) + b_L
\Big}
$$&lt;/p&gt;
&lt;p&gt;该空间包含大量可组合的非线性结构，其表达能力远超线性模型。根据通用逼近定理（Universal Approximation Theorem），只要激活函数满足适当条件（如连续、非线性、非多项式），一个具有有限宽度的前馈网络即可逼近任意连续函数：&lt;/p&gt;
&lt;p&gt;$$
\forall f \in C([0,1]^d), \forall \varepsilon &amp;gt; 0 \qquad \exists \text{ 神经网络 } g \text{ 使得 } |f - g|_\infty &amp;lt; \varepsilon.
$$&lt;/p&gt;
&lt;p&gt;这意味着非线性层使得网络能够构建分段非线性、多尺度嵌套结构，从而获得足够丰富的函数表达能力来处理实际应用。&lt;/p&gt;
&lt;p&gt;从优化角度看，若网络仅由线性层构成，其雅可比与 Hessian 的结构过于简单，梯度地形贫乏，不利于模型训练。引入非线性后，第 $k$ 层的梯度传播形式变为：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial h^{(k)}}{\partial h^{(k-1)}}
= \sigma&apos;(z^{(k)}) W_k
\qquad
z^{(k)} = W_k h^{(k-1)} + b_k
$$&lt;/p&gt;
&lt;p&gt;其中 $\sigma&apos;$ 的调制使梯度在传播过程中呈现更丰富的变化结构，从而形成更有利于优化的几何特性，使深度网络的训练成为可能并保持稳定。&lt;/p&gt;
&lt;h3&gt;常见的激活函数&lt;/h3&gt;
&lt;p&gt;在构建神经网络时，激活函数的选择会对模型的表达能力、梯度传播特性以及训练稳定性产生重要影响。不同激活函数在光滑性、梯度饱和、输出范围以及数值稳定性等方面各具特点。下文将介绍几种在实际应用中被广泛采用的激活函数及其核心性质。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sigmoid 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sigmoid 函数是最早应用于神经网络的激活函数之一，其形式为：&lt;/p&gt;
&lt;p&gt;$$
\sigma(x) = \frac{1}{1 + e^{-x}}
$$&lt;/p&gt;
&lt;p&gt;Sigmoid 函数将输入映射到 $(0,1)$ ，具备平滑可微的特性，因此在概率建模或二分类输出层中仍然常见。然而Sigmoid 函数的梯度在 $|x|$ 增大时迅速趋近于零，导致反向传播过程中容易出现梯度消失现象，从而限制网络的可训练深度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tanh 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tanh 函数是 Sigmoid 函数的平移缩放版本：&lt;/p&gt;
&lt;p&gt;$$
\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
$$&lt;/p&gt;
&lt;p&gt;其输出范围为 $(-1,1)$ ，在零点附近呈更陡峭的响应，相比 Sigmoid 函数更适合用于隐藏层。然而 Tanh 函数同样存在饱和区域，仍可能在深层网络中引发梯度衰减。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ReLU 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ReLU（Rectified Linear Unit）函数是现代深度学习中使用最广泛的激活函数，其形式为：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{ReLU}(x) = \max(0, x)
$$&lt;/p&gt;
&lt;p&gt;ReLU 函数在正半轴保持线性，不会饱和，使其在深度网络中能维持较好的梯度传播效率。同时，其稀疏性特征（大量输出为零）在一定程度上有助于提升表示能力和训练稳定性。然而，负半轴梯度为零可能导致 “神经元死亡（dead ReLU）” 现象，参数可能无法再更新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Leaky ReLU 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为缓解 ReLU 函数在负半轴完全 “关闭” 的问题，Leaky ReLU 函数引入一个小的负向斜率：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{LeakyReLU}(x) =
\begin{cases}
x, &amp;amp; x \ge 0 \
\alpha x, &amp;amp; x &amp;lt; 0
\end{cases}
\quad \alpha \in (0,1)
$$&lt;/p&gt;
&lt;p&gt;这种设计减少了死神经元的概率，使梯度在全域保持非零，有助于更稳定的深层训练。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Parametric ReLU 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PReLU 函数将 Leaky ReLU 函数的斜率参数设为可学习变量：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{LeakyReLU}(x) =
\begin{cases}
x, &amp;amp; x \ge 0 \
a x, &amp;amp; x &amp;lt; 0
\end{cases}
\quad a \text{ 可学习}
$$&lt;/p&gt;
&lt;p&gt;能够根据数据自动调节负半轴的响应程度，使网络在不同任务中获得更灵活的非线性结构，但也会引入额外参数，需要一定正则化以避免过拟合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GELU 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GELU（Gaussian Error Linear Unit）函数是近年来在大规模预训练模型中广泛使用的激活函数（如 BERT、Vision Transformer）。其近似形式为：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{GELU}(x) = x \Phi(x)
$$&lt;/p&gt;
&lt;p&gt;其中 $\Phi(x)$ 是标准正态分布的累积分布函数。GELU 函数具有平滑、概率式门控的特性，使得在输入较大时接近线性，在输入较小时平滑地抑制信号。相比 ReLU 函数，其曲线更自然地反映 “按概率保留” 信息的机制，适用于庞大模型的稳定训练。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Softplus 函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Softplus 函数是 ReLU 函数的光滑近似：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{Softplus}(x) = \log(1 + e^x)
$$&lt;/p&gt;
&lt;p&gt;与 ReLU 函数不同，Softplus 函数在全域可微，梯度连续，对于某些需要光滑优化几何结构的任务具有优势。但其计算成本略高，且在大规模神经网络中不如 ReLU 常见。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;损失函数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;损失函数&lt;/strong&gt; 用于衡量模型预测结果与真实观测之间的差异，是神经网络训练过程中优化的核心目标。设模型输出为 $\hat{y} = f_\theta(x)$ ，真实标签为 $y$ ，则单个样本的损失可写为：&lt;/p&gt;
&lt;p&gt;$$
\ell(\hat{y}, y)
$$&lt;/p&gt;
&lt;p&gt;在整个数据集上，总损失函数为：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}(\theta) = \sum_{i=1}^{n} \ell(\hat{y}_i, y_i)
$$&lt;/p&gt;
&lt;p&gt;训练的目标是通过优化参数 $\theta$ 来最小化总损失：&lt;/p&gt;
&lt;p&gt;$$
\hat{\theta} = \arg\min_{\theta} \mathcal{L}(\theta)
$$&lt;/p&gt;
&lt;h3&gt;极大似然估计&lt;/h3&gt;
&lt;p&gt;许多常见的损失函数都可以从 &lt;strong&gt;极大似然估计&lt;/strong&gt;（Maximum Likelihood Estimation，简称 MLE）中得到自然解释。其关键观点是：神经网络不仅是一个函数拟合器，更可以理解为一个参数化的概率模型。假设数据集 $\mathcal{D} = {(x_i, y_i)}_{i=1}^n$ 独立同分布，并且每个标签的条件分布由参数 $\theta$ 控制，即：&lt;/p&gt;
&lt;p&gt;$$
P(y_i | x_i, \theta)
$$&lt;/p&gt;
&lt;p&gt;那么整个数据集的联合概率为：&lt;/p&gt;
&lt;p&gt;$$
P(\mathcal{D} | \theta) = \prod_{i=1}^{n} P(y_i | x_i, \theta)
$$&lt;/p&gt;
&lt;p&gt;MLE 的目标是选择能使数据最 “可能” 出现的参数：&lt;/p&gt;
&lt;p&gt;$$
\hat\theta = \arg\max_\theta P(\mathcal{D}|\theta)
$$&lt;/p&gt;
&lt;p&gt;为了便于数值计算，一般对似然取对数，将连乘转化为求和，得到对数似然：&lt;/p&gt;
&lt;p&gt;$$
\ell(\theta) = \sum_{i=1}^{n} \log P(y_i | x_i, \theta)
$$&lt;/p&gt;
&lt;p&gt;将损失定义为负对数似然，即：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}(\theta) = - \sum_{i=1}^{n} \log P(y_i | x_i, \theta)
$$&lt;/p&gt;
&lt;p&gt;此时 &lt;strong&gt;最小化损失函数&lt;/strong&gt; 就等价于 &lt;strong&gt;最大化对数似然&lt;/strong&gt; ，从而实现 MLE。&lt;/p&gt;
&lt;p&gt;这一视角解释了许多经典损失函数的统计来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;回归任务&lt;/strong&gt;：若假设观测噪声服从高斯分布，则 MLE 等价于最小化均方误差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分类任务&lt;/strong&gt;：若类别服从多项分布，则 MLE 自然导出交叉熵损失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成模型&lt;/strong&gt;：KL 散度等价于对数似然的推广，可理解为最大化真实分布在模型分布下的概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，损失函数不仅仅是 “需要优化的量” ，它从统计学角度反映了模型对数据生成机制的假设。通过最小化损失，我们实际上在执行最大似然估计，使模型的预测分布尽可能贴近训练数据的真实分布。借助梯度下降类优化方法，这一过程得以高效实现，并为模型带来良好的拟合能力与泛化性能。&lt;/p&gt;
&lt;h3&gt;常见的损失函数&lt;/h3&gt;
&lt;p&gt;在神经网络训练中，损失函数用于量化模型预测结果与真实观测之间的差异。损失函数不仅定义了优化目标，也反映了模型对数据生成过程的概率假设。不同损失函数适用于不同任务和数据分布，下文将介绍几种经典且常用的损失函数，并分析它们的来源、推导及设计动机。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;均方误差（Mean Squared Error）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;均方误差是回归问题中最常用的损失函数，用于衡量预测值与真实值的平方差。设模型预测输出为 $\hat{y} = f_\theta(x)$ ，真实值为 $y$ ，单样本损失为：&lt;/p&gt;
&lt;p&gt;$$
\ell_{\text{MSE}}(\hat{y}, y) = (\hat{y} - y)^2
$$&lt;/p&gt;
&lt;p&gt;均方误差的设计来源于极大似然估计。假设观测值服从高斯分布：&lt;/p&gt;
&lt;p&gt;$$
y \sim \mathcal{N}(\hat{y}, \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;则条件概率为：&lt;/p&gt;
&lt;p&gt;$$
P(y | x, \theta) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\Big(-\frac{(y - \hat{y})^2}{2\sigma^2}\Big)
$$&lt;/p&gt;
&lt;p&gt;对数似然函数为：&lt;/p&gt;
&lt;p&gt;$$
\ell(\theta) = \sum_{i=1}^n \log P(y_i | x_i, \theta) \propto - \sum_{i=1}^n (y_i - \hat{y}_i)^2
$$&lt;/p&gt;
&lt;p&gt;因此，最小化均方误差等价于最大化高斯似然函数。MSE 适合连续值预测，梯度平滑，优化稳定，但对异常值敏感。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;交叉熵损失（Cross-Entropy Loss）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;交叉熵损失常用于分类问题，用于衡量预测概率分布 $\hat{y}$ 与真实类别分布 $y$ 的差异。单样本损失为：&lt;/p&gt;
&lt;p&gt;$$
\ell_{\text{CE}}(\hat{y}, y) = - \sum_c y_c \log \hat{y}_c
$$&lt;/p&gt;
&lt;p&gt;假设类别服从多项分布，则每个样本的条件概率为：&lt;/p&gt;
&lt;p&gt;$$
P(y | x, \theta) = \prod_c \hat{y}_c^{y_c}
$$&lt;/p&gt;
&lt;p&gt;对数似然函数为：&lt;/p&gt;
&lt;p&gt;$$
\ell(\theta) = \sum_i \sum_c y_{i,c} \log \hat{y}_{i,c}
$$&lt;/p&gt;
&lt;p&gt;最大化对数似然即最小化交叉熵损失，因此交叉熵损失是多项分布下的 MLE。它能够对概率预测直接建模，并提供平滑可导的梯度，适合梯度优化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;焦点损失（Focal Loss）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;焦点损失是交叉熵损失的一种改进，主要用于处理类别不平衡问题。单样本损失为：&lt;/p&gt;
&lt;p&gt;$$
\ell_{\text{Focal}}(\hat{y}, y) = - (1 - \hat{y}_t)^\gamma \log \hat{y}_t
$$&lt;/p&gt;
&lt;p&gt;其中 $\hat{y}_t$ 是真实类别的预测概率，$\gamma &amp;gt; 0$ 为调节因子。&lt;/p&gt;
&lt;p&gt;焦点损失在极大似然的基础上引入难样本加权机制，降低易分类样本的贡献，使模型更关注难分类样本。本质上仍是对数似然优化的变体，但适用于长尾类别或严重不平衡的数据集。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;KL 散度损失（KL Divergence Loss）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KL 散度损失用于衡量两个概率分布 $P$（真实分布）和 $Q$（预测分布）之间的差异：&lt;/p&gt;
&lt;p&gt;$$
\ell_{\text{KL}}(P \parallel Q) = \sum_i P(i) \log \frac{P(i)}{Q(i)}
$$&lt;/p&gt;
&lt;p&gt;其设计来源于信息论，量化预测分布与真实分布的偏离。在概率建模中，最小化 KL 散度等价于最大化对数似然的推广，即在分布匹配任务中寻找最优参数，使预测分布尽量接近目标分布。常用于生成模型、知识蒸馏等场景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;梯度下降&lt;/h2&gt;
&lt;p&gt;在训练神经网络时，我们的目标是找到一组参数，使得模型的损失函数尽可能小。形式上，这可以表示为一个优化问题：&lt;/p&gt;
&lt;p&gt;$$
\theta^* = \arg\min_\theta L(\theta)
$$&lt;/p&gt;
&lt;p&gt;然而现实中的损失函数往往非常复杂：它是由无数层线性变换、激活函数、归一化等嵌套组合而成的高维非凸函数，既没有封闭形式，也无法像传统统计模型那样通过推导直接解出最优点。再加上参数量动辄成千上万甚至上亿，任何依赖 Hessian 或二阶信息的方式在计算上都会变得不现实。&lt;/p&gt;
&lt;p&gt;尽管如此，我们仍然可以利用一个可靠的局部信息 ———— 梯度。梯度告诉我们在当前参数位置附近，损失函数的增长趋势；我们只需要朝着与梯度相反的方向前进，就能让损失下降。也就是说，梯度下降并不是在全局范围内 “找到最优解” ，而是在复杂地形中做一种 &lt;strong&gt;连续的、可迭代的局部下降&lt;/strong&gt; 过程：每一步都基于当前梯度进行微调，把参数推向 “更低的方向” 。&lt;/p&gt;
&lt;p&gt;于是就有了最基础的更新规则：&lt;/p&gt;
&lt;p&gt;$$
\theta \leftarrow \theta - \eta \nabla_\theta L(\theta)
$$&lt;/p&gt;
&lt;p&gt;这里的学习率 $\eta$ 决定了每一步的步幅：步子太大可能直接越过谷底，使训练发散；步子太小则下降缓慢。由于梯度可以通过链式法则高效计算，我们可以在每个 mini-batch 上反复执行上述更新，从而逐步逼近损失函数的低谷。这种方式不仅计算成本可控，也非常适合参数量庞大的深度学习模型。&lt;/p&gt;
&lt;h3&gt;反向传播&lt;/h3&gt;
&lt;p&gt;为了让梯度下降在神经网络中真正可行，我们必须解决一个核心问题：&lt;strong&gt;梯度从哪里来&lt;/strong&gt; 。虽然损失函数是所有层共同作用的结果，但每一层的参数对最终损失的贡献并不是直接可见的。尤其在深度模型中，几十层甚至上百层的网络结构，使得手动推导每个参数的偏导数几乎不可能。因此，我们需要一种系统化的方法，让梯度从网络输出一路传回输入端，沿途计算每个中间变量的贡献。&lt;/p&gt;
&lt;p&gt;这正是 &lt;strong&gt;反向传播（Backpropagation）&lt;/strong&gt; 的目的。其核心思想非常直观：如果前向传播是 “从输入一路计算到输出” ，那么反向传播就是 “从损失函数开始，把梯度沿着计算图回传” 。&lt;/p&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;所有参数指导此次损失函数的更新，而损失函数的变化反过来指导所有参数的更新。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于神经网络可以看作多层函数的复合，链式法则告诉我们：对任意一层求导时，只需将上游梯度与自身局部导数相乘即可。反向传播在网络中不断重复这一过程：先计算损失对输出的梯度，然后沿计算路径逐层回流，每经过一层，就根据该层的局部结构（如线性变换、激活函数、卷积等）计算新的梯度，并传递给前一层。最终，每个参数都会得到属于自己的梯度，而这些梯度正是梯度下降更新所需要的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cneural-network%5C%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD1.png&quot; alt=&quot;反向传播图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在计算机中，&lt;strong&gt;计算图（Computation Graph）&lt;/strong&gt; 模块化了整个反向传播过程。每个计算节点负责一个简单操作，梯度沿图结构逐层回传，使网络的梯度计算既系统化又高效。前向传播时，计算图记录每一次加法、乘法、矩阵运算、激活函数等操作的输出，形成数据流图：输入从图底部进入，经过各层线性或非线性变换，最终得到输出。每个节点只执行简单操作，但所有节点组合起来构成完整前向计算路径，同时保存中间变量以备反向传播使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cneural-network%5C%E8%AE%A1%E7%AE%97%E5%9B%BE1.png&quot; alt=&quot;计算图图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;反向传播时，梯度从损失函数顶端沿图结构回流。每个节点利用前向传播保存的中间值和局部变换规则，计算局部导数并传递梯度给输入节点。链式法则在此体现得非常自然：节点只需知道自身局部导数和上游梯度，即可计算并传递下游梯度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cneural-network%5C%E8%AE%A1%E7%AE%97%E5%9B%BE2.png&quot; alt=&quot;计算图图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;计算图的模块化让高维复杂函数拆解成可处理的小块，使反向传播在计算机中得以高效执行。无论网络结构多复杂，包括注意力机制、归一化层甚至自定义运算，只要每个算子实现自己的前向与反向规则，整个网络的梯度都能被自动、稳定地计算。现代深度学习框架（如 PyTorch、TensorFlow）正是基于这种 &lt;strong&gt;自动求导（autograd）&lt;/strong&gt; 系统构建的：它们维护一张动态计算图，并在需要梯度时沿图回溯。&lt;/p&gt;
&lt;p&gt;通过计算图，梯度下降成为训练深度神经网络的可行途径。它将看似无法直接求导的复杂函数拆解成可处理的小计算单元，从而实现高效、稳定的训练。&lt;/p&gt;
&lt;h3&gt;梯度下降优化&lt;/h3&gt;
&lt;p&gt;基础的梯度下降（包括随机梯度下降或 mini-batch 形式）虽然结构简单、易于实现，但在实际的深度学习训练中常表现出明显的局限性。最突出的问题是 &lt;strong&gt;收敛效率不足&lt;/strong&gt;：深度神经网络的损失函数通常高度非凸，具有明显的 &lt;strong&gt;各向异性曲率（Anisotropic Curvature）&lt;/strong&gt;。在高曲率方向，梯度下降因步长受限而推进缓慢；在低曲率方向，又容易出现剧烈振荡，使整体收敛速度受制于最 “难走” 的方向。&lt;/p&gt;
&lt;p&gt;此外，传统梯度下降为所有参数统一设置一个全局学习率，这在高维空间中显然不够灵活。不同参数的梯度特性往往差异巨大：某些参数长期保持大梯度，而另一些参数则极为稀疏或变化缓慢。在这种情况下，单一学习率会导致某些维度更新过度，而另一些维度更新不足，直接影响训练效果。&lt;/p&gt;
&lt;p&gt;深度模型的损失景观还广泛存在 &lt;strong&gt;鞍点（saddle point）&lt;/strong&gt; 、&lt;strong&gt;平坦区域（plateau）&lt;/strong&gt; 、甚至 &lt;strong&gt;梯度消失区域（vanishing gradient region）&lt;/strong&gt;。在这些区域内，梯度的方向信息几乎不提供有效下降方向，使得纯梯度下降极易停滞在不足以代表局部极小点甚至欠优化的区域附近。&lt;/p&gt;
&lt;p&gt;基于上述挑战，各类优化方法从不同维度对梯度下降进行了加强和扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二阶方法（如 Newton 法）&lt;/strong&gt; 尝试利用 Hessian 的局部曲率信息给出更接近最优步长的更新&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动量型方法（Momentum, Nesterov）&lt;/strong&gt; 通过引入累积梯度，使优化过程具备 “惯性” ，在低曲率方向加速、在高曲率方向抑制振荡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自适应学习率方法（AdaGrad, RMSProp, Adam）&lt;/strong&gt; 通过历史梯度的统计量为每个参数维度动态调整步长，使优化能够自动适应数据稀疏性与梯度尺度差异&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些方法共同构成了现代深度学习训练中最常见、最核心的优化策略，使得在高维非凸环境下的优化不再完全依赖简单梯度，而是能更有效地探索损失函数的几何结构，从而获得更快、更稳定的收敛。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;牛顿法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;牛顿法是一种二阶优化方法，其核心思想是利用梯度和 Hessian 矩阵（目标函数的二阶导数信息）来加速收敛。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
\theta_{t} = \theta_{t-1} - H^{-1}&lt;em&gt;{t-1} \nabla f(\theta&lt;/em&gt;{t-1})
$$&lt;/p&gt;
&lt;p&gt;其中 $\nabla f(\theta_{t-1})$ 是梯度，$H_{t-1}$ 是 Hessian 矩阵，即目标函数在 $\theta_{t-1}$ 处的二阶导数矩阵。&lt;/p&gt;
&lt;p&gt;通过引入 Hessian，牛顿法不仅考虑梯度方向，还利用曲率信息调整步长，使优化沿曲率适宜的方向快速收敛。直观比喻：想象在山谷中行走，不仅看坡度（梯度），还考虑坡面弯曲程度（曲率），每一步都更精准地接近最低点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;牛顿法的局限性&lt;/strong&gt;：计算和存储 Hessian 在高维空间中非常昂贵，而且 Hessian 可能不可逆或条件数差，容易导致数值不稳定。因此在深度学习中，牛顿法通常不直接使用，而是衍生出拟牛顿法（如 BFGS）或结合一阶方法的混合策略。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动量法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;动量法通过引入 “惯性” ，让参数更新不仅依赖当前梯度，还参考过去的更新方向，从而加速收敛并抑制震荡。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
v_t = \beta v_{t-1} + \eta \nabla f(\theta_{t-1}) \quad \theta_t = \theta_{t-1} - v_t
$$&lt;/p&gt;
&lt;p&gt;其中 $v_t$ 是累积的动量，$\beta \in [0,1)$ 是动量系数，$\eta$ 是基础学习率。&lt;/p&gt;
&lt;p&gt;动量的作用类似物理惯性，如果梯度连续指向某个方向，动量会让更新 “加速前进” ，如果梯度方向频繁变化，动量会抑制震荡。直观比喻：想象一个小球滚动下山，小球会沿山谷逐渐加速，而不会因为局部凸起轻易停下来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动量法的局限性&lt;/strong&gt;：需要调节 $\beta$ 和 $\eta$ 的组合，过大会导致过冲，过小则动量作用不明显。Nesterov 方法在此基础上做了改进。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nesterov 加速梯度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nesterov 的创新在于 “预见未来” 。我们可以在计算梯度之前，先沿动量方向进行一步预测，然后在预测位置计算梯度。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
v_t = \beta v_{t-1} + \eta \nabla f(\theta_{t-1} - \beta v_{t-1}) \quad \theta_t = \theta_{t-1} - v_t
$$&lt;/p&gt;
&lt;p&gt;与普通动量法不同，梯度不是在当前参数位置计算，而是在 “预测的未来位置” 计算，从而在动量方向上进行修正。直观比喻：就像开车时，不仅根据当前速度和方向踩油门，还提前观察前方路况来调整，避免过冲或过慢。Nesterov 往往比普通动量法收敛更快且更稳定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AdaGrad&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AdaGrad 的核心思想是为每个参数维度自适应地调整学习率，使得稀疏梯度参数可以走得更快，而频繁更新的参数步长自动减小。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
\theta_{t,i} = \theta_{t-1,i} - \frac{\eta}{\sqrt{\sum_{k=1}^{t} g_{k,i}^2} + \epsilon} g_{t,i}
$$&lt;/p&gt;
&lt;p&gt;其中 $g_{t,i}$ 是第 $i$ 个参数在第 $t$ 步的梯度，$\eta$ 是基础学习率，$\epsilon$ 是防止除零的小常数。&lt;/p&gt;
&lt;p&gt;累积梯度 $\displaystyle \sum_{k=1}^{t} g_{k,i}^2$ 会让频繁更新的参数步长自动减小，而稀疏梯度参数保持较大步长。直观比喻：对于常走的路自动缩短步子，对于没走过的路保持大步，使稀疏参数也能有效训练。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AdaGrad 的局限性&lt;/strong&gt;：累积梯度不断增大导致后期步长变得过小，因此 RMSProp 对此做了改进。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RMSProp&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RMSProp 是 AdaGrad 的改进版，它通过指数加权平均只关注近期梯度，避免步长过早减小。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
s_t = \gamma s_{t-1} + (1-\gamma) g_t^2 \quad \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{s_t + \epsilon}} g_t
$$&lt;/p&gt;
&lt;p&gt;其中 $s_t$ 是梯度平方的指数加权平均，$\gamma$ 通常取 0.9 左右。&lt;/p&gt;
&lt;p&gt;相比 AdaGrad，RMSProp 可以保持步长在训练后期仍然合理，尤其适合非平稳目标函数，如循环神经网络。直观比喻：像只参考最近的路况来调整步幅，而不是累积所有历史变化，使步伐保持灵活且稳健。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Adam&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Adam 将动量法与 RMSProp 结合，既考虑梯度的一阶矩（动量），又考虑二阶矩（梯度平方的指数平均），从而自适应调整每个参数步长。更新公式为：&lt;/p&gt;
&lt;p&gt;$$
m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \quad v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2
$$&lt;/p&gt;
&lt;p&gt;$$
\hat{m}_t = \frac{m_t}{1-\beta_1^t} \quad \hat{v}&lt;em&gt;t = \frac{v_t}{1-\beta_2^t} \quad \theta_t = \theta&lt;/em&gt;{t-1} - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}
$$&lt;/p&gt;
&lt;p&gt;其中 $m_t$ 和 $v_t$ 分别是一阶和二阶矩的指数加权平均，$\hat{m}_t, \hat{v}_t$ 是偏置修正。&lt;/p&gt;
&lt;p&gt;Adam 的优势是收敛快、对超参数不敏感，并且对稀疏梯度也友好。直观比喻：既沿惯性方向前进（动量），又根据最近路况自动调节步幅（RMS），像是智能化的行走策略，既快速又稳健。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adam 的局限性&lt;/strong&gt;：在某些凸优化问题上可能不如 SGD 收敛到全局最优，常结合学习率衰减使用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;梯度消失&lt;/h3&gt;
&lt;p&gt;梯度消失是指在网络反向传播过程中，梯度逐层变得非常小，几乎接近零，导致靠近输入层的参数更新极其缓慢，网络训练困难甚至停滞。&lt;/p&gt;
&lt;p&gt;数学上，如果一个深度网络有 $L$ 层，梯度计算可表示为链式法则：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial \theta_1} = \frac{\partial L}{\partial h_L} \cdot \frac{\partial h_L}{\partial h_{L-1}} \cdot \cdots \cdot \frac{\partial h_2}{\partial h_1} \cdot \frac{\partial h_1}{\partial \theta_1}
$$&lt;/p&gt;
&lt;p&gt;如果每一层的导数 $\frac{\partial h_i}{\partial h_{i-1}}$ 小于 1，则连乘 $L$ 层后，梯度会迅速衰减：&lt;/p&gt;
&lt;p&gt;$$
\prod_{i=1}^L \frac{\partial h_i}{\partial h_{i-1}} \approx 0
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用合适的激活函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sigmoid 和 tanh 在输入过大或过小时导数接近零，会加剧梯度消失。&lt;/li&gt;
&lt;li&gt;ReLU 及其变体（Leaky ReLU、ELU）能保持较大梯度，有助于缓解问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;权重初始化策略&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xavier/Glorot 初始化：适合 tanh 激活，使前向输出和反向梯度方差稳定。&lt;/li&gt;
&lt;li&gt;Kaiming He 初始化：适合 ReLU 激活，保证梯度在深层传播时不衰减。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;归一化层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BatchNorm 或 LayerNorm 可以稳定各层输入分布，使梯度更容易传播。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;残差连接（Residual Connection）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 ResNet 中，通过跳跃连接 $h_{l+1} = h_l + F(h_l)$ 让梯度能够直接流向前面层，从而显著缓解梯度消失。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;关于参数初始化方法可以观看下面两个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=343521101&amp;amp;bvid=BV1r94y1Q7eG&amp;amp;cid=778240021&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=301032447&amp;amp;bvid=BV1PF411K7nb&amp;amp;cid=778242757&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;梯度爆炸&lt;/h3&gt;
&lt;p&gt;梯度爆炸是指在反向传播过程中，梯度快速增大，导致参数更新过大，使训练不稳定甚至发散。&lt;/p&gt;
&lt;p&gt;同样用链式法则，如果每层导数大于 1，则连乘会导致梯度呈指数增长：&lt;/p&gt;
&lt;p&gt;$$
\prod_{i=1}^L \frac{\partial h_i}{\partial h_{i-1}} \gg 1
$$&lt;/p&gt;
&lt;p&gt;这种现象在循环神经网络（RNN）、LSTM 以及深层前馈网络中尤为常见。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;梯度裁剪（Gradient Clipping）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将梯度限制在预设阈值范围内：&lt;/p&gt;
&lt;p&gt;$$
g \leftarrow g \cdot \frac{\text{clip_norm}}{\max\bigl(\text{clip_norm},,\lVert g\rVert_2\bigr)}
$$&lt;/p&gt;
&lt;p&gt;常用于 RNN、LSTM、Transformer 等网络。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;合理的权重初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;避免初始权重过大，使前向输出和梯度不会指数放大。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用稳定的激活函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ReLU 系列通常比 Sigmoid/Tanh 更稳健，能减缓梯度指数增长。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;归一化层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BatchNorm、LayerNorm 可以限制每层激活值范围，从而间接控制梯度规模。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;关于归一化层可以观看下面两个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115194411878768&amp;amp;bvid=BV1hqpjzrEmT&amp;amp;cid=32345950291&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=401088977&amp;amp;bvid=BV12d4y1f74C&amp;amp;cid=1126872119&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;神经网络层级结构&lt;/h1&gt;
&lt;p&gt;神经网络由多种类型的层组合而成，每种层在模型中承担不同功能，相互协作以实现复杂的特征学习和表示能力。有的层负责线性变换和特征映射，将输入信号转换为更适合后续处理的形式；有的层专注于局部特征提取和空间结构分析，能够捕捉数据中的模式与结构信息；还有一些层用于稳定训练过程，通过调整激活分布或梯度传递，使网络收敛更快、更平稳；另外一些层则专注于防止模型过拟合，提高泛化能力，确保网络在未见数据上也能保持良好表现。&lt;/p&gt;
&lt;p&gt;这种层的组合不仅使神经网络具有强大的表达能力，也让模型能够在高维、非线性的复杂任务中学习到有效特征。不同类型的层通常相互配合：例如卷积层提取局部特征，池化层进行下采样，归一化层保持数值稳定，正则化层防止过拟合，最终线性层将抽象特征映射到具体任务输出。这种模块化的设计思路，使得神经网络既灵活又高效，能够适应图像、语音、文本等各种不同类型的数据。&lt;/p&gt;
&lt;p&gt;接下来，我们将按照功能类别，逐步介绍神经网络中常用的各类层及其作用、计算原理和直观理解。&lt;/p&gt;
&lt;h2&gt;全连接层&lt;/h2&gt;
&lt;p&gt;全连接层通常指线性变换层（Linear Transformation Layer），是神经网络中最基础的模块。它的核心功能是对输入向量进行线性映射，将输入特征组合成新的特征表示：&lt;/p&gt;
&lt;p&gt;$$
y = W x + b
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 是输入向量，$W$ 是权重矩阵，$b$ 是偏置向量，$y$ 是输出向量。通过训练，网络学习到最合适的 $W$ 和 $b$，使输出能够反映输入特征之间的关系。&lt;/p&gt;
&lt;p&gt;直观上，可以把线性层看作 “信号混合器” ，它把输入中的各类信号按不同权重组合成新的信号，再交给下一层处理。单独的线性层无法表达复杂非线性关系，但与激活函数组合后，就能构建深层网络，实现强大的特征表示能力。全连接层广泛用于分类和回归任务，尤其是网络的最后输出层。需要注意的是，参数量会随输入和输出维度增加而快速增长，因此通常与卷积层或降维层结合使用以平衡效率和表达能力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

# 定义一个线性层
# 输入维度 128，输出维度 64
fc_layer = nn.Linear(in_features=128, out_features=64)

# 输入数据，batch_size=32, 特征维度=128
x = torch.randn(32, 128)

# 前向计算
y = fc_layer(x)
print(y.shape)  # 输出: torch.Size([32, 64])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;卷积类层&lt;/h2&gt;
&lt;p&gt;卷积层不仅仅是单一的卷积操作。在深度学习中，根据计算效率、参数量、感受野和任务需求，卷积层有多种变体，每种都有特定的优势和适用场景。例如，普通卷积适用于基础特征提取，组卷积可以显著降低计算量，深度可分离卷积则在轻量化网络中表现优异，而空洞卷积可以在不增加参数的情况下扩大感受野。通过这些不同的卷积层，神经网络能够灵活地提取多尺度、多通道的特征，从而在图像识别、语义分割、生成模型等任务中发挥核心作用。常见卷积类层包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;普通卷积（Conv2d）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;普通二维卷积是最基本的卷积操作，用于从输入特征图中提取局部特征。其计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_{i,j,k} = \sum_{c=1}^{C_\text{in}} \sum_{m=1}^{K_h} \sum_{n=1}^{K_w} x_{c,i+m,j+n} \cdot W_{k,c,m,n} + b_k
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 是输入特征图，$W$ 是卷积核权重，$b$ 是偏置，$C_\text{in}$ 是输入通道数，$K_h, K_w$ 是卷积核高宽，输出特征图的通道数为卷积核个数 $k$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
x = torch.randn(1, 3, 32, 32)  # batch=1, C=3, H=32, W=32
y = conv(x)
print(y.shape)  # torch.Size([1, 16, 32, 32])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;组卷积（Group Convolution）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;组卷积将输入通道分为若干组，每组独立进行卷积，从而减少参数量和计算量。其计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y^{(g)} = \text{Conv}(x^{(g)}, W^{(g)}) + b^{(g)}
$$&lt;/p&gt;
&lt;p&gt;其中 $x^{(g)}$ 和 $W^{(g)}$ 分别是第 $g$ 组的输入通道和卷积核权重，输出 $y^{(g)}$ 为该组卷积结果。组卷积广泛用于 ResNeXt 等网络结构中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conv_group = nn.Conv2d(32, 64, kernel_size=3, groups=4, padding=1)
x = torch.randn(1, 32, 32, 32)
y = conv_group(x)
print(y.shape)  # torch.Size([1, 64, 32, 32])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;深度可分离卷积（Depthwise Separable Conv）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;深度可分离卷积将卷积操作拆分为两步：先进行深度卷积（每个输入通道独立卷积），再进行逐点卷积（1×1 卷积用于融合通道），以降低计算量和参数量。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y = \text{PointwiseConv}(\text{DepthwiseConv}(x))
$$&lt;/p&gt;
&lt;p&gt;其中 $\text{DepthwiseConv}$ 对每个通道独立处理，$\text{PointwiseConv}$ 用于通道间线性组合。该操作广泛用于轻量化网络如 MobileNet。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 深度卷积
depthwise = nn.Conv2d(32, 32, kernel_size=3, groups=32, padding=1)
# 逐点卷积
pointwise = nn.Conv2d(32, 64, kernel_size=1)
x = torch.randn(1, 32, 32, 32)
y = depthwise(x)
y = pointwise(y)
print(y.shape)  # torch.Size([1, 64, 32, 32])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;转置卷积（Transposed Conv）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;转置卷积用于上采样操作，将小尺寸特征图映射到更大尺寸，常用于生成模型（如 GAN、Autoencoder）或语义分割中。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y = \text{TransposedConv}(x, W) + b
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为输入特征图，$W$ 为卷积核权重，$b$ 为偏置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conv_trans = nn.ConvTranspose2d(16, 8, kernel_size=3, stride=2, padding=1, output_padding=1)
x = torch.randn(1, 16, 16, 16)
y = conv_trans(x)
print(y.shape)  # torch.Size([1, 8, 32, 32])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空洞卷积（Atrous Conv）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;空洞卷积在卷积核内部插入间隔（空洞），以扩大感受野而不增加参数量。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_{i,j} = \sum_{m,n} x_{i+d \cdot m, j+d \cdot n} \cdot W_{m,n}
$$&lt;/p&gt;
&lt;p&gt;其中 $d$ 是空洞率，$x$ 为输入特征图，$W$ 为卷积核权重。空洞卷积常用于语义分割和时序建模（如 WaveNet）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dilated_conv = nn.Conv2d(3, 16, kernel_size=3, dilation=2, padding=2)
x = torch.randn(1, 3, 32, 32)
y = dilated_conv(x)
print(y.shape)  # torch.Size([1, 16, 32, 32])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;池化类层&lt;/h2&gt;
&lt;p&gt;池化层的主要作用是对特征图进行下采样，从而减小空间尺寸，降低计算量和参数量，同时增强网络对平移或微小变形的鲁棒性。通过池化，网络能够保留最重要的特征信息，而忽略局部微小变化，从而提升泛化能力。常见的池化层有多种形式，例如最大池化（Max Pooling）提取局部最显著特征，平均池化（Average Pooling）则关注局部整体信息，而全局池化（Global Pooling）可以将整个特征图压缩为单个数值，用于分类任务的特征汇总。这些不同类型的池化层使网络在保持关键特征的同时，有效降低了计算复杂度。常见池化层包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最大池化（Max Pooling）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最大池化在池化窗口内取最大值，以保留最显著的特征。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_{i,j} = \max_{(p,q)\in \text{window}} x_{i+p,j+q}
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为输入特征图，$\text{window}$ 为池化区域，$y$ 为下采样后的输出特征图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
x = torch.randn(1, 16, 32, 32)
y = max_pool(x)
print(y.shape)  # torch.Size([1, 16, 16, 16])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;平均池化（Average Pooling）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;平均池化在池化窗口内计算平均值，以保留局部特征的整体分布。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_{i,j} = \frac{1}{N} \sum_{(p,q)\in \text{window}} x_{i+p,j+q}
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为输入特征图，$\text{window}$ 为池化区域，$N$ 为窗口内元素数量，$y$ 为下采样后的输出特征图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
x = torch.randn(1, 16, 32, 32)
y = avg_pool(x)
print(y.shape)  # torch.Size([1, 16, 16, 16])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局平均池化（Global Average Pooling）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;全局平均池化对整个特征图进行平均操作，通常用于分类网络的最后一层，以替代全连接层。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_c = \frac{1}{H \cdot W} \sum_{i=1}^{H} \sum_{j=1}^{W} x_{c,i,j}
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为输入特征图，$H$ 和 $W$ 分别为高度和宽度，$c$ 表示通道索引，$y_c$ 为输出的每个通道特征值。该操作能够将空间信息压缩为通道级别的全局表示，常用于 ResNet、Inception 等网络的分类层。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gap = nn.AdaptiveAvgPool2d((1,1))
x = torch.randn(1, 64, 8, 8)
y = gap(x)
print(y.shape)  # torch.Size([1, 64, 1, 1])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局最大池化（Global Max Pooling）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;全局最大池化对整个特征图取最大值，用于强调最显著的特征。计算公式为：&lt;/p&gt;
&lt;p&gt;$$
y_c = \max_{i,j} x_{c,i,j}
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为输入特征图，$c$ 表示通道索引，$i,j$ 遍历整个空间维度，$y_c$ 为输出的每个通道最大值。该操作可将空间信息压缩为通道级别的代表特征，有助于强化关键模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gmp = nn.AdaptiveMaxPool2d((1,1))
x = torch.randn(1, 64, 8, 8)
y = gmp(x)
print(y.shape)  # torch.Size([1, 64, 1, 1])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;归一化层&lt;/h2&gt;
&lt;p&gt;在深度网络中，随着信号在层与层之间传递，每一层的输入分布会不断发生变化，这会使训练变得不稳定、梯度传播困难。归一化层的设计初衷是让中间表示保持数值分布的稳定，从而加速训练、改善梯度流动，并提升网络整体的可优化性。根据归一化作用的维度不同，它有多种形式，其中最常见的是 Batch Normalization 和 Layer Normalization。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Batch Normalization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Batch Normalization 是在 CNN 和许多前馈网络中使用最广泛的归一化方式。它对每个通道，在整个 mini-batch 维度上计算均值和方差，使同一通道的数值在不同样本之间保持分布一致。其核心操作是对每个 channel 执行标准化：&lt;/p&gt;
&lt;p&gt;$$
\hat{x} = \frac{x - \mu_{\text{batch}}}{\sqrt{\sigma_{\text{batch}}^2 + \epsilon}}
$$&lt;/p&gt;
&lt;p&gt;随后通过可训练参数恢复表达能力：&lt;/p&gt;
&lt;p&gt;$$
y = \gamma \hat{x} + \beta
$$&lt;/p&gt;
&lt;p&gt;BN 在大 batch 下表现良好，能有效稳定梯度、加速收敛，并在 CNN 中成为事实标准组件。然而当 batch 较小或输入是序列数据（如 NLP）时，BN 的效果会明显下降，因为小 batch 使统计量不稳定，而序列模型中不同位置之间并不适合共享 batch 统计量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

# 用于 CNN 的 2D BatchNorm
bn = nn.BatchNorm2d(num_features=64)

x = torch.randn(32, 64, 32, 32)  # batch=32, channels=64, feature map 32x32
y = bn(x)

print(y.shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Layer Normalization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Layer Normalization 则完全避免依赖 batch，它在每个样本内部进行归一化，对该样本的所有通道（或特征维度）求均值与方差。这意味着每条样本的归一化计算彼此独立，不受 batch 大小影响。因此 LN 非常适合 Transformer、RNN 以及各种序列模型。&lt;/p&gt;
&lt;p&gt;其标准化方式为：&lt;/p&gt;
&lt;p&gt;$$
\hat{x} = \frac{x - \mu_{\text{layer}}}{\sqrt{\sigma_{\text{layer}}^2 + \epsilon}}
$$&lt;/p&gt;
&lt;p&gt;同样有可训练的缩放与偏移：&lt;/p&gt;
&lt;p&gt;$$
y = \gamma \hat{x} + \beta
$$&lt;/p&gt;
&lt;p&gt;由于 LN 在单个样本内部进行统计，它在 NLP 和注意力模型中具有极高的稳定性，能够让网络在长序列和复杂结构中保持可训练性，也被视为 Transformer 成功的重要因素之一。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

# 用于 Transformer/MLP 的 LayerNorm
ln = nn.LayerNorm(normalized_shape=128)

x = torch.randn(32, 10, 128)  # batch=32, seq_len=10, feature_dim=128
y = ln(x)

print(y.shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;正则化层&lt;/h2&gt;
&lt;p&gt;正则化层的核心目标是提升模型的泛化能力，避免网络在训练数据上过拟合。随着网络深度和参数规模不断增长，模型往往会记住训练集中的噪声和偶然模式，使测试性能下降。正则化层通过在训练过程中引入随机性、稀疏性或归约机制，让网络学到更加稳健的特征表示。&lt;/p&gt;
&lt;p&gt;深度学习中使用最广泛的正则化方式是 &lt;strong&gt;Dropout&lt;/strong&gt; ，此外还包括 DropConnect、Stochastic Depth 等更深度化的结构性随机化手段。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dropout&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dropout 的核心思想是在训练时随机 “丢弃” 一部分神经元，使网络不能依赖某些特定特征，从而降低 Co-Adaptation（特征共适应）。在前向传播中，Dropout 会对每个神经元以一定概率 $p$ 将其置零：&lt;/p&gt;
&lt;p&gt;$$
\tilde{h}_i = h_i \cdot z_i,\qquad z_i \sim \text{Bernoulli}(1-p)
$$&lt;/p&gt;
&lt;p&gt;为了保持期望一致，训练时会进行缩放（Inverted Dropout）：&lt;/p&gt;
&lt;p&gt;$$
\tilde{h}_i = \frac{h_i \cdot z_i}{1-p}
$$&lt;/p&gt;
&lt;p&gt;在推理阶段，则不执行任何随机丢弃操作，使网络表现确定且稳定。&lt;/p&gt;
&lt;p&gt;Dropout 能有效缓解过拟合，因此经常用于全连接网络、MLP block 或 Transformer 的前馈层。对于卷积网络，Dropout 的效果相对弱于 BatchNorm + 数据增强，但在较深的 CNN 中仍然有一定应用价值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

# Dropout 概率 p=0.5
dropout = nn.Dropout(p=0.5)

x = torch.randn(4, 10)   # 一个 batch 的全连接输入
y = dropout(x)

print(y)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DropConnect&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;与 Dropout 丢弃激活值不同，DropConnect 随机丢弃的是权重。其计算形式为：&lt;/p&gt;
&lt;p&gt;$$
\tilde{W} = W \cdot M \qquad M_{ij} \sim \text{Bernoulli}(1-p)
$$&lt;/p&gt;
&lt;p&gt;然后用被随机裁剪后的权重进行前向传播。这是一种更激进的正则化方式，但在实践中使用较少，只在部分模型如一些 RNN 变体中出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

class DropConnectLinear(nn.Linear):
    def __init__(self, in_features, out_features, p=0.5):
        super().__init__(in_features, out_features)
        self.p = p

    def forward(self, x):
        if self.training:
            mask = torch.bernoulli(torch.full_like(self.weight, 1-self.p))
            w = self.weight * mask
        else:
            w = self.weight
        return x @ w.t() + self.bias

layer = DropConnectLinear(128, 64, p=0.3)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stochastic Depth&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在深层网络中，某些残差块可以在训练时随机跳过，从而形成 “动态深度” 。对于一个残差块：&lt;/p&gt;
&lt;p&gt;$$
y = x + F(x)
$$&lt;/p&gt;
&lt;p&gt;Stochastic Depth 会以概率 $p$ 直接跳过该残差分支：&lt;/p&gt;
&lt;p&gt;$$
y =
\begin{cases}
x + F(x), &amp;amp; \text{with prob } 1-p\
x, &amp;amp; \text{with prob } p
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;这种方法特别适用于深层 ResNet、Vision Transformer，使模型在训练时表现为较浅网络，更易优化，而在推理时保持完整深度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyTorch 示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

class StochasticDepth(nn.Module):
    def __init__(self, p):
        super().__init__()
        self.p = p

    def forward(self, x, residual):
        if self.training and torch.rand(1) &amp;lt; self.p:
            return x
        return x + residual

# 使用（伪示例）
sd = StochasticDepth(p=0.2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Softmax 函数是如何设计的？（如何用最大熵原理推出 Softmax 函数？）&lt;/p&gt;
&lt;p&gt;在多分类任务中，我们希望模型输出一组 &lt;strong&gt;概率&lt;/strong&gt;：这些概率不仅要非负、总和为 1，还要能够响应输入的 “证据” 差异。最大熵原理提供了一条自然路径：在满足必要约束的前提下，选择 &lt;strong&gt;熵最大的分布&lt;/strong&gt; ，从而避免引入额外偏见。&lt;/p&gt;
&lt;h3&gt;最大熵建模&lt;/h3&gt;
&lt;p&gt;假设模型的前向计算得到一组实数（logits）：&lt;/p&gt;
&lt;p&gt;$$
z = (z_1, \dots, z_K)
$$&lt;/p&gt;
&lt;p&gt;我们希望基于这些分数构造一个概率分布：&lt;/p&gt;
&lt;p&gt;$$
p = (p_1,\dots, p_K) \quad p_k\ge 0,\ \sum_k p_k = 1
$$&lt;/p&gt;
&lt;p&gt;最大熵原理告诉我们：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在所有满足约束的概率分布中，应选择 &lt;strong&gt;熵最大&lt;/strong&gt; 的那个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;熵的定义为：&lt;/p&gt;
&lt;p&gt;$$
H(p)= -\sum_{k=1}^K p_k \log p_k
$$&lt;/p&gt;
&lt;p&gt;为了让概率分布能够体现 logits 提供的信息，需要加入一个 &lt;strong&gt;期望约束&lt;/strong&gt;（常称 “能量约束” ）：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^K p_k z_k = \mu
$$&lt;/p&gt;
&lt;p&gt;其中 $\mu$ 不必显式求出，它由拉格朗日乘子自然吸收。&lt;/p&gt;
&lt;p&gt;因此最大化问题写作：&lt;/p&gt;
&lt;p&gt;$$
\max_{p_k\ge 0} -\sum_{k} p_k \log p_k
$$&lt;/p&gt;
&lt;p&gt;$$
\text{s.t. } \sum_k p_k = 1 \quad \sum_k p_k z_k = \mu
$$&lt;/p&gt;
&lt;h3&gt;拉格朗日乘子求解&lt;/h3&gt;
&lt;p&gt;构造拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}(p, \alpha, \beta)
= -\sum_k p_k \log p_k + \alpha\left(\sum_k p_k -1\right) + \beta\left(\sum_k p_k z_k -\mu\right)
$$&lt;/p&gt;
&lt;p&gt;对 $p_k$ 求偏导并令其为零：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial \mathcal{L}}{\partial p_k}
= -(\log p_k + 1) + \alpha + \beta z_k = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\log p_k = \alpha - 1 + \beta z_k
$$&lt;/p&gt;
&lt;p&gt;指数化后可以得到：&lt;/p&gt;
&lt;p&gt;$$
p_k = e^{\alpha - 1} e^{\beta z_k} = C e^{\beta z_k}
$$&lt;/p&gt;
&lt;p&gt;利用归一化条件：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^K p_k = 1
\quad\Rightarrow\quad
C = \frac{1}{\sum_j e^{\beta z_j}}
$$&lt;/p&gt;
&lt;p&gt;最终得到 Softmax 的一般形式：&lt;/p&gt;
&lt;p&gt;$$
\boxed{
p_k = \frac{e^{\beta z_k}}{\sum_j e^{\beta z_j}}
}
$$&lt;/p&gt;
&lt;p&gt;其中 $\beta = 1/T$ 是温度系数，用于控制分布的尖锐程度。最常用的设定是 $\beta=1$ ，得到标准 Softmax：&lt;/p&gt;
&lt;p&gt;$$
\boxed{
p_k = \frac{e^{z_k}}{\sum_j e^{z_j}}
}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于更详细的讲解可以参考以下视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=889408481&amp;amp;bvid=BV1cP4y1t7cP&amp;amp;cid=379457443&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;神经网络相关教程&lt;/h1&gt;
&lt;h2&gt;激活函数相关&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115774953887638&amp;amp;bvid=BV1NXBLB2EE2&amp;amp;cid=34956052957&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;损失函数相关&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115644930463746&amp;amp;bvid=BV1GHS1BzE6J&amp;amp;cid=34424164218&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;梯度下降相关&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115694389697492&amp;amp;bvid=BV14kmxBiEja&amp;amp;cid=34635778203&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115796965595669&amp;amp;bvid=BV1mSvkBQEyr&amp;amp;cid=35037253112&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115830503249253&amp;amp;bvid=BV1sgivBFEDq&amp;amp;cid=35162751839&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>【ACM 算法随笔】两数之和思想</title><link>https://xingguang641.com/posts/acm/acm-note/two-sum-idea/two-sum-idea/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/two-sum-idea/two-sum-idea/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇博客写作灵感来源于灵神的两数之和理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=305008442&amp;amp;bvid=BV1bP411c7oJ&amp;amp;cid=888954096&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h1&gt;两数之和题目讲解&lt;/h1&gt;
&lt;p&gt;两数之和是 LeetCode 上编号为 $1$ 的开山题目，常被称作算法题中的 &lt;code&gt;Hello World!&lt;/code&gt; 。它表面看起来十分基础，却并不只停留在新手练习的层面。之所以值得专门展开讨论，是因为 &lt;strong&gt;“两数之和” 背后所蕴含的思想具有极强的泛用性&lt;/strong&gt; ，并且贯穿于大量经典算法问题之中，衍生出多种重要的技巧与模型。许多题目你也许早已独立做过，却未必意识到它们之间存在着紧密而统一的内在逻辑。&lt;/p&gt;
&lt;p&gt;接下来，我们将以 “两数之和” 这一核心问题为主线，逐步串联起一系列相关题型与解题思路。希望通过这样的梳理，能够帮助我们跳脱出单个题目的视角，对那些看似零散却本质相通的问题形成更加 &lt;strong&gt;系统化、结构化的认识&lt;/strong&gt; ，从而建立更稳固的解题框架。&lt;/p&gt;
&lt;h2&gt;两数之和母题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/two-sum/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 &lt;strong&gt;和为目标值&lt;/strong&gt; target 的那 &lt;strong&gt;两个&lt;/strong&gt; 整数，并返回它们的数组下标。&lt;/p&gt;
&lt;p&gt;你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$2 \leq nums.length \leq 10^4$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq target \leq 10^9$&lt;/li&gt;
&lt;li&gt;只存在一个有效答案&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $n$ 和 $target$ ，分别表示数组长度和目标值。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n \quad target$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_n$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出两个整数 $i$ 和 $j$ 表示答案，且要满足 $i &amp;lt; j$ 。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 9
2 7 11 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 6
3 2 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 6
3 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;由于这个题目本身难度较低，我们不妨在它的基础上稍作扩展。具体来说，我们将原题中 “只存在一个有效答案” 的限制移除，允许出现 &lt;strong&gt;多个不同的数对&lt;/strong&gt; 和为 target，并要求我们统计所有满足条件的数对数量。&lt;/p&gt;
&lt;p&gt;为了统计所有满足条件的数对数量，我们可以先思考一个直观的解法：对于数组中每个元素，都去查找它左侧部分中是否存在与之配对的目标值。这样可以枚举所有可能的配对关系，但时间复杂度会达到 $O(n^2)$ 。&lt;/p&gt;
&lt;p&gt;不过我们可以换一个视角：从左到右遍历数组，那么在访问当前元素之前，它左侧的所有信息都已知。如果我们能够在遍历过程中 &lt;strong&gt;实时记录已经出现过的数字及其出现次数&lt;/strong&gt; ，那么对于当前元素，我们只需花费 $O(1)$ 的时间查询其配对目标是否已经出现过，并累加数量即可。&lt;/p&gt;
&lt;p&gt;首先，我们明确题目所要求的条件：&lt;/p&gt;
&lt;p&gt;$$
nums[i] + nums[j] = target
$$&lt;/p&gt;
&lt;p&gt;然后我们将关于 $j$ 的部分移动到等式另一侧，可以得到：&lt;/p&gt;
&lt;p&gt;$$
nums[i] = target - nums[j]
$$&lt;/p&gt;
&lt;p&gt;当我们遍历数组时，如果能够实时记录已经出现过的数字 $nums[j]$ 的出现次数，那么对于当前数字 $nums[i]$ ，只需要查询它所对应的配对值 $target - nums[i]$ 之前出现了多少次，就能直接得出以 $i$ 为右端点所贡献的有效数对数量。我们可以得到如下的算法流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查询（Query）&lt;/strong&gt;：在哈希表中查找是否存在键为 $target - nums[i]$ 的记录，若存在则累计答案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录（Update）&lt;/strong&gt;：将当前数字 $nums[i]$ 更新到哈希表中（出现次数 + 1 ），作为后续数字的配对目标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同理，我们也可以查询 $nums[i]$ ，记录 $target - nums[i]$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 1e4 + 100;
int n, target;
int a[MAXN];

int main(){
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; target;
    for (int i = 0; i &amp;lt; n; i++){
        cin &amp;gt;&amp;gt; a[i];
    }

    int ans = 0;  
    unordered_map&amp;lt;int, int&amp;gt; counts;
    for (int i = 0; i &amp;lt; n; i++){
        if (counts.count(target - a[i])){
            ans += counts[target - a[i]];
        }
        counts[a[i]]++;
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码还可以改成下面这样，效果是差不多的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int ans = 0;  
unordered_map&amp;lt;int, int&amp;gt; counts;
for (int i = 0; i &amp;lt; n; i++){
    if (counts.count(a[i])){
        ans += counts[a[i]];
    }
    counts[target - a[i]]++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;多元组序列问题&lt;/h1&gt;
&lt;p&gt;在掌握 “两数之和” 的解法后，我们实际上已经触及了序列处理的核心逻辑：如何在看似杂乱的线性序列中，高效统计满足特定约束的多元素子序列。这类问题统称为 &lt;strong&gt;多元组序列问题&lt;/strong&gt; 。无论是统计三元组还是探寻更高维的组合，它们在算法底层并非孤立存在，而是 “两数之和” 思想向高维空间的自然延伸。&lt;/p&gt;
&lt;p&gt;当我们将视野从两元扩展至三元组（满足 $i &amp;lt; j &amp;lt; k$ 的 $A_i, A_j, A_k$ ）时，&lt;strong&gt;降维思想&lt;/strong&gt; 展现出了极强的普适性。如果我们固定最右侧端点 $k$ ，原问题便瞬间退化为：在动态变化的区间 $[0, k-1]$ 中寻找满足特定条件的二元组。这意味着，复杂的三元组统计并非全新的课题，而是建立在 “两数之和” 基础之上的嵌套结构。随着外层枚举的单向推进，内层始终维持着一个标准的、可随步进实时维护的低维系统。&lt;/p&gt;
&lt;p&gt;除了固定右端点，我们还可以切换至 &lt;strong&gt;中心枚举&lt;/strong&gt; 的视角重新审视，并引入 &lt;strong&gt;idx 数组&lt;/strong&gt; 来辅组枚举。这里的 $idx$ 数组存储的是元素下标，并按照对应元素的大小进行排序。它让我们在不破坏元素原始位置信息的前提下，获得了一套按数值排列的索引映射。然后我们依次枚举 $idx$ 中的元素作为中心点 $j$ ，并将之前处理过的 $idx$ 进行分拣：早于 $j$ 的放入左侧候选集，晚于 $j$ 的放入右侧候选集。由于这个过程是按数值大小推进的，左右两个集合提取出的元素天然有序，从而将棘手的偏序匹配转化为两个有序序列间的线性扫描。&lt;/p&gt;
&lt;h2&gt;三数累加和问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/3sum-with-multiplicity/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;arr&lt;/code&gt; ，以及一个整数 &lt;code&gt;target&lt;/code&gt; 作为目标值，返回满足 &lt;code&gt;i &amp;lt; j &amp;lt; k&lt;/code&gt; 且 &lt;code&gt;arr[i] + arr[j] + arr[k] == target&lt;/code&gt; 的元组 &lt;code&gt;i, j, k&lt;/code&gt; 的数量。&lt;/p&gt;
&lt;p&gt;由于结果会非常大，请返回 $10^9 + 7$ 的模。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$3 \leq arr.length \leq 3000$&lt;/li&gt;
&lt;li&gt;$0 \leq arr[i] \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq target \leq 300$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $target$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad target$&lt;/p&gt;
&lt;p&gt;$arr_1 \quad arr_2 \quad \ldots \quad arr_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出满足条件的三元组的数量。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10 8
1 1 2 2 3 3 4 4 5 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;20
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;三数之和的做法并不复杂，我们可以先固定第三个下标 $k$ ，把它当作当前三元组的右端点，然后只考虑区间 $[0, k-1]$ 中的元素。在 $k$ 已经确定的情况下，问题就变成了：在前缀区间中寻找一对下标 $i &amp;lt; j$ ，使得它们对应的两个数与 $arr[k]$ 之和等于目标值。也就是说，每固定一个 $k$ ，就在它左侧跑一次两数之和。&lt;/p&gt;
&lt;p&gt;原问题中关于数值的约束可以写为：&lt;/p&gt;
&lt;p&gt;$$
arr[i] + arr[j] + arr[k] = target
$$&lt;/p&gt;
&lt;p&gt;将与 $k$ 相关的项移到等式右侧，就得到：&lt;/p&gt;
&lt;p&gt;$$
arr[i] + arr[j] = target - arr[k]
$$&lt;/p&gt;
&lt;p&gt;此时，问题就完全转化为一个标准的 &lt;strong&gt;两数之和计数问题&lt;/strong&gt;：在前缀区间 $[0, k-1]$ 中，统计有多少对下标满足 $i &amp;lt; j$ 且对应的元素之和等于 $target - arr[k]$ 。因此，只要我们能够在遍历过程中维护左侧区间中各个数值的出现次数，就可以在常数时间内计算出当前 $k$ 所贡献的有效三元组数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 3000 + 5;
int n, target;
int arr[MAXN];

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; target;
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; arr[i];
    }

    ll ans = 0;
    for (int k = 0; k &amp;lt; n; k++) {
        unordered_map&amp;lt;int, int&amp;gt; counts;
        
        for (int j = 0; j &amp;lt; k; j++) {
            int need = target - arr[k] - arr[j];
            if (counts.count(need)) {
                ans = (ans + counts[need]) % MOD;
            }
            counts[arr[j]]++;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;有效三角形数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/valid-triangle-number/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个包含非负整数的数组 &lt;code&gt;nums&lt;/code&gt; ，返回其中可以组成三角形三条边的三元组个数。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 1000$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示可以组成三角形的个数。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
2 2 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
4 2 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;根据三角形的性质，若三边满足 $a \leq b \leq c$ ，则只需满足 $a + b &amp;gt; c$ 即可。因此，我们先对数组进行升序排序，随后固定最大边的下标 $k$ ，在区间 $[0, k-1]$ 内寻找满足条件的二元组 $(i, j)$ 。&lt;/p&gt;
&lt;p&gt;此时原问题的约束可以转化为：&lt;/p&gt;
&lt;p&gt;$$
arr[i] + arr[j] &amp;gt; arr[k]
$$&lt;/p&gt;
&lt;p&gt;在 $k$ 确定的情况下，我们可以使用 &lt;strong&gt;双指针法&lt;/strong&gt; 快速计数。将左指针 $i$ 置于 $0$ ，右指针 $j$ 置于 $k-1$ 。&lt;/p&gt;
&lt;p&gt;若当前满足 $arr[i] + arr[j] &amp;gt; arr[k]$ ，说明在 $j$ 固定的前提下，下标在 $[i, j-1]$ 范围内的所有元素均可作为最小边与 $arr[j]$ 组成三角形，其贡献的组合数为：&lt;/p&gt;
&lt;p&gt;$$
Ans = Ans + (j - i)
$$&lt;/p&gt;
&lt;p&gt;若当前两数之和不满足条件，则向右移动 $i$ 以增大数值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;统计好的三元组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-good-triplets/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个长度为 $N$ 的整数数组 &lt;code&gt;arr&lt;/code&gt; ，以及三个整数 $a$、$b$、$c$。请你统计其中 &lt;strong&gt;好三元组&lt;/strong&gt; 的数量。&lt;/p&gt;
&lt;p&gt;如果三元组 &lt;code&gt;(arr[i], arr[j], arr[k])&lt;/code&gt; 满足下列全部条件，则认为它是一个好三元组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$0 \leq i &amp;lt; j &amp;lt; k &amp;lt; N$&lt;/li&gt;
&lt;li&gt;$|arr[i] - arr[j]| \leq a$&lt;/li&gt;
&lt;li&gt;$|arr[j] - arr[k]| \leq b$&lt;/li&gt;
&lt;li&gt;$|arr[i] - arr[k]| \leq c$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 $|x|$ 表示 $x$ 的绝对值。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$3 \leq N \leq 100$&lt;/li&gt;
&lt;li&gt;$0 \leq arr[i] \leq 1000$&lt;/li&gt;
&lt;li&gt;$0 \leq a, b, c \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含四个整数 $N$ 、$a$ 、$b$ 和 $c$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad a \quad b \quad c$&lt;/p&gt;
&lt;p&gt;$arr_1 \quad arr_2 \quad \ldots \quad arr_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数，表示满足条件的好三元组的数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 7 2 3
3 0 1 1 9 7

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 0 0 1
1 1 2 2 3

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;针对这道题，最直观的策略是直接根据题目定义的三个绝对值约束进行 &lt;strong&gt;三重循环枚举&lt;/strong&gt; 。在 $N \leq 100$ 的数据规模下，$O(N^3)$ 的复杂度足以通过评测。然而，这个问题的核心价值在于其优化思路的启发性：如何通过固定特定端点，将三维相互耦合的约束转化为一维或二维的动态维护问题，是打破计算瓶颈的关键。&lt;/p&gt;
&lt;p&gt;第一种优化思路借鉴了 &lt;strong&gt;三数之和&lt;/strong&gt; 中固定右端点的策略。我们尝试固定右端点 $k$ ，并在中间点 $j$ 从 $0$ 向 $k-1$ 顺序移动的过程中实现高效统计。为了自动地满足多重约束，我们引入一个 &lt;strong&gt;动态维护的频率数组&lt;/strong&gt; 来记录历史状态。其逻辑遵循严密的先后顺序：当 $j$ 满足约束 $|arr[j] - arr[k]| \leq b$ 时，立即发起一次针对左侧合法 $i$ 的查询；查询完成后，若 $arr[j]$ 满足约束 $|arr[j] - arr[k]| \leq c$ ，则将其存入频率数组中。&lt;/p&gt;
&lt;p&gt;这种设计的巧妙之处在于，查询动作发生在当前元素入库之前，从而天然满足了下标 $i &amp;lt; j &amp;lt; k$ 的序关系。为了实现 $O(1)$ 的区间检索，我们利用前缀和数组 &lt;code&gt;pre&lt;/code&gt; 维护当前桶中 $\leq \text{num}$ 的元素个数。此时，第一个不等式约束 $|arr[i] - arr[j]| \leq a$ 转化为值域区间 $[arr[j]-a, arr[j]+a]$ 的计数问题，通过 &lt;code&gt;pre[r] - pre[l-1]&lt;/code&gt; 即可快速获取答案。这种方法将复杂度优化至 $O(N^2 + N \cdot \text{maxVal})$ ，在值域较小的场景下表现卓越。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管前面的做法非常巧妙，但没办法处理值域较大的情况，因此我们要换一个思路。绝对值问题的核心难点在于数据的无序性，但由于本题下标顺序与数值大小相互制约，我们无法直接对原数组排序。为此，我们构建一个下标数组 &lt;code&gt;idx&lt;/code&gt; 并按照其在原数组 &lt;code&gt;arr&lt;/code&gt; 中的数值进行升序排列。当我们依次枚举 &lt;code&gt;idx&lt;/code&gt; 中的每一个元素作为三元组的中间点 $j$ 时，可以将数值比当前 $arr[j]$ 更小的历史元素（即在排序数组中排在 $j$ 之前的元素）进行 &lt;strong&gt;动态分组&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在这一步中，我们只需将这些已经遍历过的、数值较小的元素，根据它们在原数组中的原始位置分配到 &lt;strong&gt;left&lt;/strong&gt; 或 &lt;strong&gt;right&lt;/strong&gt; 两个集合中。具体而言，只有原始下标 $idx[p] &amp;lt; j$ 且满足 $arr[j] - arr[idx[p]] \leq a$ 的元素才会被放入 left 集合，以此作为合法的 $i$ 候选；同理，只有原始下标 $idx[p] &amp;gt; j$ 且满足 $arr[j] - arr[idx[p]] \leq b$ 的元素才会被放入 right 集合，作为合法的 $k$ 候选。&lt;/p&gt;
&lt;p&gt;由于我们是按照 &lt;code&gt;idx&lt;/code&gt; 的升序逻辑进行遍历的，每次提取出的历史元素本身就遵循从小到大的排列，因此生成的 &lt;strong&gt;left&lt;/strong&gt; 和 &lt;strong&gt;right&lt;/strong&gt; 集合 &lt;strong&gt;天然满足有序性&lt;/strong&gt; 。此时，三元组的构建就转化为了一个典型的有序数组跨集合匹配问题，即处理 $i$ 与 $k$ 之间的最后一道约束 $|arr[i] - arr[k]| \leq c$ 。我们只需枚举 &lt;strong&gt;left&lt;/strong&gt; 中的每个元素 $x$ ，并在 &lt;strong&gt;right&lt;/strong&gt; 中寻找落在区间 $[x-c, x+c]$ 内的元素。利用 &lt;strong&gt;三指针技巧&lt;/strong&gt; 维护两个在 &lt;strong&gt;right&lt;/strong&gt; 上单调滑动的边界，即可在摆脱值域依赖的前提下，以 $O(N^2)$ 的复杂度完成所有配对统计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;数组子段和问题&lt;/h1&gt;
&lt;p&gt;在处理数组子段和问题时，一个最核心且具有普适性的观察在于，如果我们预先定义数组的前缀和序列为 pre，那么原数组中任意一段连续子数组的和 $[left,right]$ 都可以通过两个前缀项的差值来精确表达：&lt;/p&gt;
&lt;p&gt;$$
pre[right] - pre[left] = sum[left: right]
$$&lt;/p&gt;
&lt;p&gt;也就是说，一个子段的和本质上等价于 &lt;strong&gt;两个前缀和之间的差值&lt;/strong&gt; 。当我们从这个角度重新观察问题时，就会发现很多原本看起来复杂的子数组问题，其实都可以转化为 &lt;strong&gt;寻找满足某种关系的两个前缀值&lt;/strong&gt; 。换句话说，我们并不需要直接枚举整个子数组，而是只需要关注 &lt;strong&gt;前缀之间的关系&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;这样一来，数组子段和就转化为经典的 &lt;strong&gt;两数之和问题&lt;/strong&gt; 。当我们枚举右端点 $right$ 时，实际上就是固定了一个前缀值 $pre[right]$ ，此时问题往往转化为：&lt;strong&gt;是否存在某个左端点 left，使得 $pre[left]$ 与当前前缀值满足某种条件&lt;/strong&gt; 。根据题目的不同限制，我们可以用多种方式来维护这些可能的前缀值，例如使用 &lt;strong&gt;哈希表统计出现次数、双指针维护区间、或借助有序结构进行查询&lt;/strong&gt; 等。&lt;/p&gt;
&lt;p&gt;因此，从建模角度来看，很多所谓的 &lt;strong&gt;数组子段和问题&lt;/strong&gt; ，本质上都可以理解为一种 &lt;strong&gt;前缀和上的两数关系问题&lt;/strong&gt; 。当我们意识到这一点之后，许多看似不同的题目其实都可以用类似的思路来处理：枚举一侧端点，并在另一侧维护可能的前缀值集合。接下来我们就通过几个具体例子，来进一步体会这种转化在实际题目中的应用方式。&lt;/p&gt;
&lt;h2&gt;累加和为定值的最长子数组长度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/36fb0fd3c656480c92b569258a1223d5&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个无序数组 $arr$ ，其中元素是在一定范围内的任意整数。给定一个整数 $k$ ，求 $arr$ 所有子数组中累加和为 $k$ 的最长子数组长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq k \leq 10^9$&lt;/li&gt;
&lt;li&gt;$-100 \leq arr_i \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$arr_1 \quad arr_2 \quad \ldots \quad arr_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5 0
1 -2 1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;借鉴 “两数之和” 的核心思路，我们依然把子数组和的问题转化为前缀和差值。设 $pre[i]$ 表示前 $i$ 个元素的前缀和，则任意区间满足子数组和为 $k$ 的条件可以写成：&lt;/p&gt;
&lt;p&gt;$$
pre[right] - pre[left] = k \qquad (left \leq right)
$$&lt;/p&gt;
&lt;p&gt;与 “两数之和” 不同的一点在于，这里的运算是减法，它不具备 &lt;strong&gt;交换律&lt;/strong&gt; 。因此我们不能像那道题一样随意决定 “存什么” 和 “查什么” ，必须严格按照移项后的形式来设计具体的存储和查找逻辑。基于这个形式，有两种完全对称、但实现细节不同的写法。&lt;/p&gt;
&lt;h3&gt;思路一：查历史存当前&lt;/h3&gt;
&lt;p&gt;从原式移项得到：&lt;/p&gt;
&lt;p&gt;$$
pre[right] - k = pre[left]
$$&lt;/p&gt;
&lt;p&gt;当我们遍历到位置 right 时，我们需要在哈希表中 &lt;strong&gt;查找过去是否出现&lt;/strong&gt; 值为 $pre[right]-k$ 的前缀和；然后再把当前前缀和 $pre[right]$ 记录到哈希表中，作为后续位置的查询依据。&lt;/p&gt;
&lt;p&gt;由于我们存的是 &lt;strong&gt;真实前缀和&lt;/strong&gt; ，在正式开始遍历前，前缀和为 $0$ ，因此必须初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pos[0] = -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思路二：查当前存期望&lt;/h3&gt;
&lt;p&gt;把式子换另一种移项方式：&lt;/p&gt;
&lt;p&gt;$$
pre[right] = pre[left] + k
$$&lt;/p&gt;
&lt;p&gt;当我们遍历到位置 right 时，我们需要在哈希表中 &lt;strong&gt;查询过去是否需要&lt;/strong&gt; 值为 $pre[right]$ 的前缀和；接着再把未来的期望值 $pre[right] + k$ 存入哈希表，表示该元素希望遇到这个数来凑成区间和 $k$ 。&lt;/p&gt;
&lt;p&gt;由于此时存的是 &lt;strong&gt;期望值&lt;/strong&gt; ，初始前缀和为 $0$ ，它期待未来遇到值为 $k$ 的前缀和，因此初始化为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pos[k] = -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论采用哪种写法，为了找到 &lt;strong&gt;最长子数组&lt;/strong&gt; ，哈希表都只需要记录每个键 &lt;strong&gt;第一次出现的位置&lt;/strong&gt; 。这样当匹配成功时，左端点越靠前，区间长度自然越大。基于以上两类思路，我们即可写出最终代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 1e5 + 100;
int N, k;
int arr[MAXN];

int main() {
    cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; k;
    for (int i = 0; i &amp;lt; N; i++) {
        cin &amp;gt;&amp;gt; arr[i];
    }

    unordered_map&amp;lt;long long, int&amp;gt; pos;  
    pos[k] = -1; int pre = 0, ans = 0;
    for (int i = 0; i &amp;lt; N; i++) {
        pre += arr[i];

        if (pos.count(pre)) {
            ans = max(ans, i - pos[pre]);
        }

        if (!pos.count(pre + k)) {
            pos[pre + k] = i;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;和为K的子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subarray-sum-equals-k/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 $nums$ 和一个整数 $k$ ，请你统计并返回该数组中和为 $k$ 的子数组的个数。&lt;/p&gt;
&lt;p&gt;子数组是数组中元素的连续非空序列。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 2 * 10^4$&lt;/li&gt;
&lt;li&gt;$-1000 \leq nums[i] \leq 1000$&lt;/li&gt;
&lt;li&gt;$-10^7 \leq k \leq 10^7$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $k$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad k$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 2
1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;针对这道子数组求和问题，核心在于利用 &lt;strong&gt;前缀和&lt;/strong&gt; 将连续区间的求和转化为两个端点值的差值。通过引入前缀和序列 $pre$ ，题目要求的子数组和 $k$ 便等价替换为 $pre[j] - pre[i-1] = k$ 。这种转化成功地将一个需要枚举区间的 $O(N^2)$ 问题，降维成了一个寻找特定数值匹配的二元组问题，在底层逻辑上与 “两数之和” 达成了高度统一。&lt;/p&gt;
&lt;p&gt;在实现过程中，我们利用哈希表动态维护已经扫描过的前缀和及其出现频率。由于目标是寻找满足 $pre[i-1] = pre[j] - k$ 的历史状态，我们只需在遍历时随走随查、随查随储。需要特别注意的是，哈希表的初始状态应包含 &lt;code&gt;counts[0] = 1&lt;/code&gt; ，这代表了前缀和恰好等于 $k$ 、即从数组起始位置开始的合法子数组。通过这种空间换时间的策略，我们能以 $O(N)$ 的复杂度优雅地统计出所有答案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 2e4 + 100;
int N, k;
int nums[MAXN];

int main() {
    cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; k;
    for (int i = 0; i &amp;lt; N; i++){
        cin &amp;gt;&amp;gt; nums[i];
    }

    unordered_map&amp;lt;int, int&amp;gt; counts;
    int ans = 0, pre = 0; counts[k] = 1;
    for (int i = 0; i &amp;lt; N; i++){
        pre += nums[i];
        if (counts.count(pre)){
            ans += counts[pre];
        }
        counts[pre + k]++;
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;正负相同子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/545544c060804eceaed0bb84fcd992fb&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个无序数组 $arr$ ，求 $arr$ 所有子数组中正数与负数个数相等的最长子数组的长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq arr.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-100 \leq arr_i \leq 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 -2 1 1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;首先观察题目，会发现题目中出现了 &lt;strong&gt;“一样多”&lt;/strong&gt; 这个关键字。对于这种类型的问题，我们常用的技巧是将数据 &lt;strong&gt;二值化&lt;/strong&gt; ，并通过累加和为 $0$ 来表示数量相等。这个技巧非常实用，在后续的题目中会经常用到。&lt;/p&gt;
&lt;p&gt;具体做法是：将正整数看作 $1$ ，将负数看作 $-1$ ，然后寻找 &lt;strong&gt;累加和为 0 的最长子数组&lt;/strong&gt; 。需要注意的是，原数组中可能存在 $0$ ，但我们可以直接忽略，不要在转换时把等于号也加入判断（尤其注意 &lt;code&gt;if else&lt;/code&gt; 语句），否则会导致错误。这样处理后，我们就可以直接套用之前讲的 &lt;strong&gt;“累加和为定值的最长子数组长度”&lt;/strong&gt; 的代码框架来解决问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 1e5 + 100;
int N; int arr[MAXN];

int main() {
    cin &amp;gt;&amp;gt; N;
    for (int i = 0; i &amp;lt; N; i++) {
        int num; cin &amp;gt;&amp;gt; num;
        if (num &amp;lt; 0) arr[i] = -1;
        if (num &amp;gt; 0) arr[i] = 1;
    }

    unordered_map&amp;lt;long long, int&amp;gt; pos;
    pos[0] = -1; int pre = 0, ans = 0;
    for (int i = 0; i &amp;lt; N; i++) {
        pre += arr[i];

        if (pos.count(pre)) {
            ans = max(ans, i - pos[pre]);
        }

        if (!pos.count(pre)) {
            pos[pre] = i;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;良好最长时间段&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-well-performing-interval/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一份工作时间表 $hours$ ，上面记录着某一位员工每天的工作小时数。&lt;/p&gt;
&lt;p&gt;我们认为当员工一天中的工作小时数大于 8 小时的时候，那么这一天就是「劳累的一天」。&lt;/p&gt;
&lt;p&gt;所谓「表现良好的时间段」，意味在这段时间内，「劳累的天数」是严格 大于「不劳累的天数」。&lt;/p&gt;
&lt;p&gt;请你返回「表现良好时间段」的最大长度。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq hours.length \leq 10^4$&lt;/li&gt;
&lt;li&gt;$0 \leq hours[i] \leq 16$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$hours_1 \quad hours_2 \quad \ldots \quad hours_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
9 9 6 0 6 6 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
6 6 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;同样地，我们将大于 $8$ 的数值映射为 $1$ ，小于等于 $8$ 的数值映射为 $-1$ 。此时，问题转化为寻找 &lt;strong&gt;元素和大于 0 的最长子数组&lt;/strong&gt; 。引入前缀和数组 $pre$ ，子数组和大于 $0$ 等价于 $pre[left] &amp;lt; pre[right]$ 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;形式化描述&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在前缀和数组中，寻找一对索引 $(left, right)$ ，在满足 $left &amp;lt; right$ 且 $pre[left] &amp;lt; pre[right]$ 的前提下，使 $right - left$ 最大。这实际上是一个经典的 &lt;strong&gt;单调栈问题&lt;/strong&gt; ，我们需要为每一个 $right$ 找到其左侧 &lt;strong&gt;距离最远&lt;/strong&gt; 且 &lt;strong&gt;数值更小&lt;/strong&gt; 的下标 $left$ ，然后计算差值，再在这些差值中寻找最大值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这道题还有一个可以利用的特殊性质：数组中的数字只有 ±1 ，因此前缀和变化为 $1$ ，满足 &lt;strong&gt;单调连续性&lt;/strong&gt; 。利用这一性质，我们可以进一步简化算法。对于当前位置 $i$ ，若 $pre[i] &amp;gt; 0$ ，说明从数组起始位置到当前位置的整体和为正，此时可以直接得到最长长度为 $i + 1$ 。若 $pre[i] \leq 0$ ，则需要在其左侧寻找一个位置 $left$ ，使得 $pre[left] &amp;lt; pre[i]$ ，从而最大化区间长度 $i - left$ 。关键在于如何高效地确定这个 $left$ 。&lt;/p&gt;
&lt;p&gt;根据前缀和的单调连续性，前缀和从 $0$ 下降到 $pre[i]$（例如 $-5$ ）的过程中，&lt;strong&gt;必然会在更早的位置&lt;/strong&gt; 先变化为 $pre[i] + 1$（例如 $-4$ ）。也就是说，$pre[i] - 1$ 首次出现的位置一定早于 $pre[i] - 2$ 、$pre[i] - 3$ 等更小数值首次出现的位置。因此，为了使区间长度最大，我们只需关注 $pre[i] - 1$ 首次出现的位置，无需枚举所有小于 $pre[i]$ 的前缀和值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int MAXN = 1e4 + 100;
int N; int hours[MAXN];

int main(){
    cin &amp;gt;&amp;gt; N;
    for (int i = 0; i &amp;lt; N; i++){
        cin &amp;gt;&amp;gt; hours[i];
    }

    unordered_map&amp;lt;int, int&amp;gt; first;
    first[0] = -1; int pre = 0; int ans = 0;
    for (int i = 0; i &amp;lt; N; i++){
        if (hours[i] &amp;gt; 8) pre += 1;
        else pre -= 1;

        if (pre &amp;gt; 0){
            ans = max(ans, i + 1);
        }

        else if (first.count(pre - 1)){
            ans = max(ans, i - first[pre - 1]);
        }

        if (!first.count(pre)){
            first[pre] = i;
        }
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目相关拓展&lt;/h2&gt;
&lt;p&gt;如果将 &lt;strong&gt;“寻找最长子数组”&lt;/strong&gt; 的条件改为 &lt;strong&gt;“统计目标子数组个数”&lt;/strong&gt; ，我们又该如何处理呢？同样地，我们先不考虑这道题的特殊性，直接给出这道题的一般形式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;形式化描述&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在前缀和数组中，统计有多少对 $(left, right)$ 满足 $left &amp;lt; right$ 且 $pre[left] &amp;lt; pre[right]$（每个数对都是一个子数组，因此统计数对个数即可得到子数组个数）。这实际上就是 &lt;strong&gt;顺序对问题&lt;/strong&gt; ，因此可以直接使用 &lt;strong&gt;归并分治&lt;/strong&gt; 来解决这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在我们考虑如何利用 &lt;strong&gt;单调连续性&lt;/strong&gt;：由于前缀和的变化每次仅 ±1 ，当遍历到某个下标时，我们只需要知道 &lt;strong&gt;小于当前 $pre[i]$ 的前缀个数&lt;/strong&gt; 即可统计顺序对数量。基于这个特点，可以使用 &lt;strong&gt;增量法&lt;/strong&gt; 动态维护前缀信息。&lt;/p&gt;
&lt;p&gt;当 $pre$ 增加 $1$ 时，由于之前已统计了所有小于 $pre$ 的前缀个数，只需加上等于 $pre$ 的前缀数量即可；当 $pre$ 减少 $1$ 时，由于之前已统计了所有小于 $pre$ 的前缀个数，只需舍弃等于 $pre - 1$ 的前缀数量即可。由于前缀和变化的步长恒为 1，因此动态维护 &lt;strong&gt;小于当前 $pre[i]$ 的前缀个数&lt;/strong&gt; 非常高效，只需统计每种前缀值出现的次数，即可在 $O(1)$ 时间完成更新。&lt;/p&gt;
&lt;h2&gt;构造P整除数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/make-sum-divisible-by-p/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给你一个正整数数组 $nums$ ，请你移除 &lt;strong&gt;最短&lt;/strong&gt; 子数组（可以为 &lt;strong&gt;空&lt;/strong&gt; ），使得剩余元素的 &lt;strong&gt;和&lt;/strong&gt; 能被 $p$ 整除。&lt;strong&gt;不允许&lt;/strong&gt; 将整个数组都移除。请你返回你需要移除的最短子数组的长度，如果无法满足题目要求，返回 $-1$ 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子数组&lt;/strong&gt; 定义为原数组中连续的一组元素。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq nums.length \leq 10^5$&lt;/li&gt;
&lt;li&gt;$0 \leq nums[i] \leq 10^9$&lt;/li&gt;
&lt;li&gt;$1 \leq p \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $p$ 。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 6
3 1 4 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 9
6 3 5 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 3
1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 3&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;首先这道题有一个很明显的点：如果整个数组的和模 $p$ 余 $0$ ，那我们不需要移除任何数。如果整个数组的和模 $p$ 余 $r$ ，那我们就要找到累加和（取模后）为 $r$ 的最短子数组。&lt;/p&gt;
&lt;p&gt;因此我们可以得到下面这个条件：&lt;/p&gt;
&lt;p&gt;$$
pre[right] - pre[left] \equiv r \pmod{p}
$$&lt;/p&gt;
&lt;p&gt;根据两数之和的思想，我们将 $left$ 移至右侧可得：&lt;/p&gt;
&lt;p&gt;$$
pre[right] \equiv r + pre[left] \pmod{p}
$$&lt;/p&gt;
&lt;p&gt;因此我们要查询 $pre[i] % p$ 的同时，统计 $(r + pre[i]) % p$ 出现的最早位置。从 “两数之和” 的角度来看，这道题的标准写法其实很自然，虽然涉及取模运算，看似有些奇怪，但背后的数学思想非常直观。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
int N; ll p;

int main() {
    cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; p;
    vector&amp;lt;long long&amp;gt; nums(N);
    for (int i = 0; i &amp;lt; N; i++) {
        cin &amp;gt;&amp;gt; nums[i];
    }

    ll total = 0;
    for (ll x : nums) total += x;
    int r = total % p;
    if (r == 0) {
        cout &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; endl;
        return 0;
    }

    unordered_map&amp;lt;int, int&amp;gt; pos;
    pos[0] = -1; ll pre = 0; int ans = N;
    for (int i = 0; i &amp;lt; N; i++) {
        pre = (pre + nums[i]) % p;

        int need = (pre - r + p) % p;
        if (pos.count(need)) {
            ans = min(ans, i - pos[need]);
        }

        pos[pre] = i;
    }

    if (ans == N) cout &amp;lt;&amp;lt; -1 &amp;lt;&amp;lt; endl;
    else cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;树上的路径总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/path-sum-iii/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个二叉树的根节点 &lt;code&gt;root&lt;/code&gt; ，和一个整数 &lt;code&gt;targetSum&lt;/code&gt; ，求该二叉树里节点值之和等于 &lt;code&gt;targetSum&lt;/code&gt; 的 &lt;strong&gt;路径&lt;/strong&gt; 的数目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;路径&lt;/strong&gt; 不需要从根节点开始，也不需要在叶子节点结束，但是路径方向必须是向下的（只能从父节点到子节点）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Ctwo-sum-idea%5C%E6%A0%91%E4%B8%8A%E7%9A%84%E8%B7%AF%E5%BE%84%E9%97%AE%E9%A2%98.png&quot; alt=&quot;树上的路径问题图像&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;二叉树的节点个数的范围是 $[0,1000]$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq Node.val \leq 10^9$&lt;/li&gt;
&lt;li&gt;$-1000 \leq targetSum \leq 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含两个整数 $N$ 和 $targetSum$ ，其中 $N$ 表示节点个数。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示这颗树 $1 \sim N$ 节点的权值，其中 $1$ 节点为根节点。&lt;/li&gt;
&lt;li&gt;接下来的 $N - 1$ 行中，每一行都会给出两个整数，表示这两个节点之间有边相连。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N \quad targetSum$&lt;/p&gt;
&lt;p&gt;$Node_1 \quad Node_2 \quad \ldots \quad Node_N$&lt;/p&gt;
&lt;p&gt;$Node_{u_1} \quad Node_{v_1}$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$Node_{u_{N-1}} \quad Node_{v_{N-1}}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示答案。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;9 8
10 5 -3 3 2 11 3 -2 1
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这是一道典型的 “树上向下路径统计” 问题。由于路径只能从父节点走向子节点，因此在 DFS 过程中，当前递归栈上从根到当前节点形成的一条链，本质上就是一条一维序列。问题可以理解为：在这条动态路径中，寻找若干对位置，使得两者对应的路径和之差等于 &lt;code&gt;targetSum&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;设当前遍历到节点 $u$ 时，从根到 $u$ 的路径和为 &lt;code&gt;curSum&lt;/code&gt; 。若存在某个祖先节点，其对应的路径和为 &lt;code&gt;x&lt;/code&gt; ，满足：&lt;/p&gt;
&lt;p&gt;$$
curSum - x = targetSum
$$&lt;/p&gt;
&lt;p&gt;那么从该祖先之后到当前节点这一段路径就是一个合法解。因此，在 DFS 过程中维护一个哈希表，记录当前路径上每个路径和出现的次数。访问当前节点时，先计算新的 &lt;code&gt;curSum&lt;/code&gt; ，然后查询 &lt;code&gt;curSum - targetSum&lt;/code&gt; 在哈希表中出现了多少次，并将其累加到答案中。随后将当前 &lt;code&gt;curSum&lt;/code&gt; 计入哈希表，继续递归访问子树。递归返回父节点时，将当前 &lt;code&gt;curSum&lt;/code&gt; 的出现次数减一，以完成回溯，确保哈希表中始终只保存当前递归路径上的信息。&lt;/p&gt;
&lt;p&gt;整棵树只需一次 DFS，每个节点进行常数次哈希查询与更新，时间复杂度为 $O(N)$ ，空间复杂度为 $O(N)$ 。从结构上看，这类问题的关键在于路径方向单调，使得整棵树在遍历过程中始终可以被压缩成一条动态路径，从而把问题转化为路径上的 “两数之和” 在线匹配问题。这种转化方式在树上路径计数类问题中具有很强的普适性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1005;
int N; ll targetSum;
ll val[MAXN];
vector&amp;lt;int&amp;gt; G[MAXN];

void dfs(int u, int parent, ll pre) {
    pre += val[u];
    if (counts.count(pre)) {
        ans += counts[pre];
    }

    counts[pre + targetSum]++;
    for (int v : G[u]) {
        if (v == parent) continue;
        dfs(v, u, pre);
    }
    counts[pre + targetSum]--;
}

int main() {
    cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; targetSum;
    for (int i = 1; i &amp;lt;= N; i++) {
        cin &amp;gt;&amp;gt; val[i];
    }

    for (int i = 1; i &amp;lt; N; i++) {
        int u, v;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v;
        G[u].push_back(v);
        G[v].push_back(u);
    }

    unordered_map&amp;lt;ll, int&amp;gt; counts;
    counts[targetSum] = 1; ll ans = 0;

    if (N &amp;gt; 0) {
        dfs(1, 0, 0);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;相乘结果为正为负的子数组数量&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1215/B&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的整数序列 $a$ ，其中所有元素都 &lt;strong&gt;不等于 0&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;你需要计算以下两个值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;满足 $l \leq r$ 的下标对 $(l, r)$ 的数量，使得 $a_l \cdot a_{l+1} \cdots a_r$ 的乘积为 &lt;strong&gt;负数&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;满足 $l \leq r$ 的下标对 $(l,r)$ 的数量，使得 $a_l \cdot a_{l+1} \cdots a_r$ 的乘积为 &lt;strong&gt;正数&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;换句话说，你需要统计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有 &lt;strong&gt;子数组乘积为负数&lt;/strong&gt; 的数量&lt;/li&gt;
&lt;li&gt;所有 &lt;strong&gt;子数组乘积为正数&lt;/strong&gt; 的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$-10^9 \leq a_i \leq 10^9$&lt;/li&gt;
&lt;li&gt;$a_i \neq 0$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出两个整数，分别表示乘积为负数的子数组和乘积为正数的子数组的数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
5 -3 3 -1 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;8 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10
4 2 -4 3 1 2 -4 3 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;28 27
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题要求统计所有子数组中 &lt;strong&gt;乘积为正数&lt;/strong&gt; 和 &lt;strong&gt;乘积为负数&lt;/strong&gt; 的数量。由于数组中不存在 $0$ ，子数组乘积的符号完全取决于其中 &lt;strong&gt;负数的个数的奇偶性&lt;/strong&gt;：如果负数个数为偶数，则乘积为正；如果负数个数为奇数，则乘积为负。因此，本题的核心并不在于真正计算乘积，而是判断区间内负数个数的奇偶性。&lt;/p&gt;
&lt;p&gt;为了简化问题，可以先对原数组进行一次符号映射：将所有正数看作 $1$ ，所有负数看作 $-1$ 。这样数组的每个元素只表示符号信息，而不再关心具体数值。接下来再进一步利用奇偶性的特点，将 $1$ 视为 $0$ ，$-1$ 视为 $1$ 。此时问题就转化为了：对于一个由 $0$ 和 $1$ 构成的序列，统计所有子数组中 &lt;strong&gt;1 的个数为奇数或偶数&lt;/strong&gt; 的区间数量。&lt;/p&gt;
&lt;p&gt;在这种表示方式下，可以引入 &lt;strong&gt;前缀异或和&lt;/strong&gt; 。设 $pre_i$ 表示前 $i$ 个元素中 $1$ 的个数的奇偶性（即这些值的异或结果）。那么任意区间 $[l,r]$ 中 $1$ 的个数奇偶性可以表示为：&lt;/p&gt;
&lt;p&gt;$$
pre_r \oplus pre_{l-1}
$$&lt;/p&gt;
&lt;p&gt;如果结果为 $0$ ，说明区间中负数个数为偶数，乘积为正；如果结果为 $1$ ，说明区间中负数个数为奇数，乘积为负。这样一来，问题就与 &lt;strong&gt;利用前缀和统计子数组性质&lt;/strong&gt; 的经典做法完全一致，只不过这里把加法换成了异或运算。&lt;/p&gt;
&lt;p&gt;具体实现时，可以在遍历数组的过程中维护当前前缀异或值，同时记录此前出现过多少次 $0$ 和 $1$ 。如果当前前缀值为 $x$ ，那么与之前 &lt;strong&gt;相同前缀值&lt;/strong&gt; 配对的区间，其异或结果为 $0$ ，对应乘积为正；而与之前 &lt;strong&gt;不同前缀值&lt;/strong&gt; 配对的区间，其异或结果为 $1$ ，对应乘积为负。于是可以在扫描数组的同时不断累加这两类区间的数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;翻转以聚类问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc408/tasks/abc408_d&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个长度为 $N$ 的字符串 $S$ ，字符串仅由字符 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 组成。&lt;/p&gt;
&lt;p&gt;你可以进行任意次（包括 $0$ 次）如下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择一个位置 $i$（ $1 \leq i \leq N$ ），将 $S_i$ 翻转（即 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 变为 &lt;code&gt;&apos;1&apos;&lt;/code&gt;，或 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 变为 &lt;code&gt;&apos;0&apos;&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你的目标是使字符串中 &lt;strong&gt;所有的 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 至多形成一个连续区间&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;换句话说，最终字符串需要满足以下条件之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字符串中没有 &lt;code&gt;&apos;1&apos;&lt;/code&gt;（全为 &lt;code&gt;&apos;0&apos;&lt;/code&gt; ），或&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;存在一段区间 $[l, r)$ ，使得：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当且仅当 $l \leq i &amp;lt; r$ 时，$S_i = &apos;1&apos;$&lt;/li&gt;
&lt;li&gt;其它位置均为 &lt;code&gt;&apos;0&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请你求出，为了满足上述条件，&lt;strong&gt;最少需要进行多少次翻转操作&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq T \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq N \leq 2 \times 10^5$&lt;/li&gt;
&lt;li&gt;$S$ 是一个仅由 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 组成的字符串&lt;/li&gt;
&lt;li&gt;所有测试用例中 $N$ 的总和不超过 $2 \times 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一行包含一个整数 $T$ ，表示测试用例的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于每个测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示字符串长度。&lt;/li&gt;
&lt;li&gt;第二行包含一个长度为 $N$ 的字符串 $S$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$T$&lt;/p&gt;
&lt;p&gt;$N_1$&lt;/p&gt;
&lt;p&gt;$S_1$&lt;/p&gt;
&lt;p&gt;$N_2$&lt;/p&gt;
&lt;p&gt;$S_2$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$N_T$&lt;/p&gt;
&lt;p&gt;$S_T$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;对于每个测试用例，输出一行一个整数，表示最少需要的翻转次数。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
5
10011
10
1111111111
7
0000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
0
0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题可以等价地理解为一个区间选择问题。由于最终状态要求所有的 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 至多形成一个连续区间，因此我们可以假设答案对应于某个区间 $[l, r]$：该区间内的字符全部为 &lt;code&gt;&apos;1&apos;&lt;/code&gt; ，区间外的字符全部为 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 。在这种视角下，我们不再关心翻转的顺序，而只关心为了达到这一目标状态，总共需要翻转多少个字符。&lt;/p&gt;
&lt;p&gt;在区间 $[l, r]$ 内，所有原本为 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 的字符都必须被翻转成 &lt;code&gt;&apos;1&apos;&lt;/code&gt; ；而在区间外，所有原本为 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 的字符都必须被翻转成 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 。因此，翻转次数可以自然地拆分为这两部分之和。为了高效计算任意区间的代价，我们引入前缀和数组，其中 &lt;code&gt;pre0[i]&lt;/code&gt; 表示前 $i$ 个字符中 &lt;code&gt;&apos;0&apos;&lt;/code&gt; 的数量，&lt;code&gt;pre1[i]&lt;/code&gt; 表示前 $i$ 个字符中 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 的数量。&lt;/p&gt;
&lt;p&gt;当区间选为 $[l, r]$ 时，总的翻转次数可以表示为：&lt;/p&gt;
&lt;p&gt;$$
pre0[r] - pre0[l - 1] + pre1[l - 1] + pre1[n] - pre1[r]
$$&lt;/p&gt;
&lt;p&gt;对该式进行整理，可以将其拆解为一项只与右端点 $r$ 有关的部分，以及一项只与左端点 $l$ 有关的部分：&lt;/p&gt;
&lt;p&gt;$$
pre0[r] + pre1[n] - pre1[r] + \big(pre1[l - 1] - pre0[l - 1] \big)
$$&lt;/p&gt;
&lt;p&gt;这一拆分形式非常关键，它使得问题可以用 “两数之和” 的方式来处理。当我们将右端点 $r$ 固定时，前半部分相当于一个常数，此时要做的就是在所有满足 $l \leq r$ 的左端点中，找出使 $pre1[l - 1] - pre0[l - 1]$ 最大的位置。因此在从左到右扫描字符串的过程中，只需要维护截至当前位置之前该表达式的最大值，就可以在 $O(1)$ 时间内计算出以当前 $r$ 作为右端点时的最小翻转代价，只需扫描一次即可完成所有区间的枚举与答案更新。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
int n; string s;

int main(){
    int T; cin &amp;gt;&amp;gt; T;
    while (T--){
        cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; s;
        vector&amp;lt;int&amp;gt; pre0(n + 1, 0), pre1(n + 1, 0);
        for (int i = 1; i &amp;lt;= n; i++){
            pre0[i] = pre0[i - 1];
            pre1[i] = pre1[i - 1];
            if (s[i - 1] == &apos;0&apos;) pre0[i]++;
            else pre1[i]++;
        }

        int ans = INT_MAX, best = 0;
        for (int r = 1; r &amp;lt;= n; r++){
            int cur = pre0[r] + pre1[n] - pre1[r];
            ans = min(ans, cur + best);
            best = max(best, pre1[r] - pre0[r]);
        }

        cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; &apos;\n&apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;树上点配对问题&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;树上点配对问题&lt;/strong&gt; 指的是在树上统计某些特殊的点对，其核心在于如何处理 &lt;strong&gt;跨子树的交互关系&lt;/strong&gt; 。为了解决此类问题，我们可以借用 “两数之和” 的思路：在遍历每棵子树时，我们将已经遍历过的子树信息统计起来，当我们枚举到当前子树的节点时，通过维护信息的数据结构来查询需要的点，从而快速完成统计。这种在遍历过程中同步进行信息检索与样本入库的动态模式，通过利用 &lt;strong&gt;DFS 的序关系&lt;/strong&gt; 巧妙地将 $O(n^2)$ 的两两匹配转化为高效的在线查找，使得原本杂乱的跨子树交互变得有序。&lt;/p&gt;
&lt;p&gt;并且 &lt;strong&gt;树上路径问题&lt;/strong&gt; 也可以类比为树上点配对问题，因为树上路径的端点固定，树上路径就唯一确定，因此 &lt;strong&gt;统计路径本质就是在统计点对&lt;/strong&gt; 。在基础的路径统计中，我们经常通过两点的信息与 &lt;strong&gt;LCA&lt;/strong&gt; 的信息共同刻画路径属性。例如，若要统计长度为 $k$ 的路径，本质上是在寻找满足 $dep[u] + dep[v] - 2 \cdot dep[LCA] = k$ 的点对。通过在递归回溯时维护哈希表计数器，我们可以高效地捕捉这些由端点定义的路径信息。&lt;/p&gt;
&lt;p&gt;当统计条件涉及复杂的 &lt;strong&gt;全局约束&lt;/strong&gt; 时，局部配对往往难以维持效率。一个典型的例子是统计 &lt;strong&gt;长度不超过 $k$ 的路径总数&lt;/strong&gt; ，由于路径端点的分布极其分散且路径长度计算依赖于不同的 LCA，简单的子树合并容易导致复杂度退化。为了更高效地处理这类需要统揽全局的路径关系，我们通常需要引入&lt;a href=&quot;https://xingguang641.com/posts/acm/acm-type/graph-problems/tree-algorithms/tree-algorithms/#%E6%A0%91%E4%B8%8A%E7%82%B9%E5%88%86%E6%B2%BB%E9%97%AE%E9%A2%98&quot;&gt;点分治算法&lt;/a&gt;，通过不断拆解树的重心，将全局路径拆分为经过特定重心的子问题，从而在 $O(\log n)$ 的层级内完成对全树点对的精准覆盖。&lt;/p&gt;
&lt;h2&gt;北斗玄阵交感力&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.matiji.net/exam/brushquestion/77/4693/305EE97B0D5E361DE6A28CD18C929AF0&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一棵包含 $n$ 个节点的树，节点编号为 $1 \sim n$ ，其中编号为 $1$ 的节点是根节点，每个节点 $i$ 铭刻着一个数值 $a_i$ 。定义 $lca(x, y)$ 为节点 $x$ 和 $y$ 的最近公共祖先，$popcnt(x)$ 为整数 $x$ 在二进制表示下 $1$ 的数量。&lt;/p&gt;
&lt;p&gt;请计算以下表达式的值（结果对 $10^9 + 7$ 取模）：&lt;/p&gt;
&lt;p&gt;$$
\left( \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} (a_i + a_j)^{popcnt(a_{lca})} \right) \pmod{10^9 + 7}
$$&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$1 \leq n \leq 3 \times 10^5$&lt;/li&gt;
&lt;li&gt;$1 \leq a_i \leq 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含多行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $n$ ，表示数组长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $n$ 个整数，表示数组元素。&lt;/li&gt;
&lt;li&gt;接下来 $n-1$ 行，每行包含两个整数 $x, y$ ，表示编号为 $x$ 和 $y$ 的星台之间有一条边。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$n$&lt;/p&gt;
&lt;p&gt;$a_1 \quad a_2 \quad \ldots \quad a_n$&lt;/p&gt;
&lt;p&gt;$x_1 \quad y_1$&lt;/p&gt;
&lt;p&gt;$\ldots$&lt;/p&gt;
&lt;p&gt;$x_{n-1} \quad y_{n-1}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示计算结果。&lt;/p&gt;
&lt;h3&gt;Sample Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;7
2 33 4 7 1 66 7
1 2
1 3
1 4
3 5
3 6
4 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3450
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;这道题最直观的切入点是以每个节点作为 LCA 统计一次答案，但整个问题的难点在于如何高效计算公式中的高次幂项。若直接对子树内的点对进行双重循环，时间复杂度将达到 $O(N^2)$ ，在这道题的数据范围内无法通过。因此我们可以尝试利用和式变换的思想，将 $i$ 和 $j$ 实现逻辑上的解耦。&lt;/p&gt;
&lt;p&gt;对于幂次函数，最经典的优化手段就是 &lt;strong&gt;二项式展开&lt;/strong&gt; 。我们可以将求和内部的 $(a_i + a_j)^{P_x}$ 进行展开：&lt;/p&gt;
&lt;p&gt;$$
(a_i + a_j)^{P_x} = \sum_{k=0}^{P_x} C_{P_x}^k \cdot a_i^k \cdot a_j^{P_x-k}
$$&lt;/p&gt;
&lt;p&gt;将展开式代入原有的双重求和公式后，我们得到的是一个三重求和结构。然后将对 $k$ 的枚举提到最外层。由于组合数 $C_{P_x}^k$ 仅与 $k$ 相关，可以进一步将其提至求和号外部：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i \in T_x} \sum_{j \in Sub_v} \sum_{k=0}^{P_x} C_{P_x}^k a_i^k a_j^{P_x-k} = \sum_{k=0}^{P_x} C_{P_x}^k \left( \sum_{i \in T_x} \sum_{j \in Sub_v} a_i^k a_j^{P_x-k} \right)
$$&lt;/p&gt;
&lt;p&gt;接下来利用 &lt;strong&gt;乘法分配律&lt;/strong&gt; ，观察到在内部的双重求和中，$a_i^k$ 与 $j$ 无关，$a_j^{P_x-k}$ 与 $i$ 无关。因此我们可以将原有的嵌套求和结构重组，将对 $i$ 和 $j$ 的求和分别收拢至各自对应的函数项上，从而将相互耦合的点对枚举转化为两个独立集合的汇总信息乘积：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=0}^{P_x} C_{P_x}^k \left( \sum_{i \in T_x} a_i^k \right) \left( \sum_{j \in Sub_v} a_j^{P_x-k} \right)
$$&lt;/p&gt;
&lt;p&gt;通过这个变换，我们成功将 $i$ 和 $j$ 分离到了两个独立的乘积项中。在具体实现时，我们可以借鉴 “两数之和” 的动态维护思想：对于每个节点 $x$ ，维护其子树内所有权值的 $k$ 次幂之和 $f(x, k)$ 。当合并子树 $Sub_v$ 时，利用当前已合并集合 $T_x$ 的幂次汇总表与新子树进行 $O(P)$ 的合并计算：&lt;/p&gt;
&lt;p&gt;$$
Ans = \sum_{k=0}^{P_x} C_{P_x}^k \cdot f(T_x, k) \cdot f(Sub_v, P_x-k)
$$&lt;/p&gt;
&lt;p&gt;当每一个新子树 $Sub_v$ 准备合并进当前集合时，我们将其视为 “两数之和” 中的第二个数 $j$ ，而之前维护的汇总表则代表了所有符合条件的 $i$ 。通过查表获取 $f(T_x, k)$ ，配合当前子树的 $f(Sub_v, P_x-k)$ ，即可在 $O(P)$ 的时间内完成该阶段所有点对贡献的累计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main() {

}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【ACM 算法随笔】归并排序与归并分治</title><link>https://xingguang641.com/posts/acm/acm-note/merge-sort/merge-sort/</link><guid isPermaLink="true">https://xingguang641.com/posts/acm/acm-note/merge-sort/merge-sort/</guid><description>记录一些 ACM 常用技巧</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;归并排序基本原理&lt;/h1&gt;
&lt;p&gt;1945 年，约翰·冯·诺依曼（John von Neumann）首次提出了归并排序算法。作为 &lt;strong&gt;分治思想在排序问题上的经典应用&lt;/strong&gt; ，它在算法理论和实际工程中都具有重要意义。归并排序的核心理念可以概括为 &lt;strong&gt;分而治之、治而合之&lt;/strong&gt; 。在排序过程中，算法首先不断将原始序列拆解为更小的子序列，&lt;strong&gt;直到每个子序列只包含一个元素为止&lt;/strong&gt; 。由于 &lt;strong&gt;单个元素本身就具有天然有序性&lt;/strong&gt; ，这一步实际上完成了对所有局部子问题的最小化。&lt;/p&gt;
&lt;p&gt;当拆分过程结束后，算法便进入 &lt;strong&gt;归并阶段&lt;/strong&gt; 。在这一阶段中，程序会从递归的底部开始，将已经有序的子序列两两合并，并在合并时通过比较操作保证结果依旧有序。随着合并任务逐层向上推进，多个局部有序解逐渐汇聚为一个完整的全局有序序列，从而完成整个排序过程。得益于这一 &lt;strong&gt;严谨且结构化的流程&lt;/strong&gt; ，归并排序能够保证 &lt;strong&gt;排序稳定性&lt;/strong&gt; ，即相同元素的相对顺序不会发生改变。同时它也非常适合用于 &lt;strong&gt;外部排序场景&lt;/strong&gt; ，如处理数据量巨大到无法一次性加载进内存的任务。归并排序不仅是理解分治思想的最佳范例之一，也是 &lt;strong&gt;现代高效排序算法的重要理论基础&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;排序算法图解&lt;/h2&gt;
&lt;p&gt;为了更清晰地理解归并排序的 &lt;strong&gt;整体思路及执行细节&lt;/strong&gt; ，我们将从一个具体的初始数列 $[8,4,5,7,1,3,6,2]$ 开始，沿着算法 &lt;strong&gt;实际运行的轨迹&lt;/strong&gt; ，通过图示依次展示它在 &lt;strong&gt;不断拆分与逐层合并&lt;/strong&gt; 过程中的变化。借助这样的 &lt;strong&gt;可视化展开&lt;/strong&gt; ，我们不仅能看到序列如何被递归地划分成更小的部分，也能直观理解这些部分又是怎样一步步重新组合成一个完全有序的结果。&lt;/p&gt;
&lt;h3&gt;递归拆分阶段&lt;/h3&gt;
&lt;p&gt;归并排序首先会从 &lt;strong&gt;整体入手&lt;/strong&gt; ，将序列逐步对半拆分，就像不断把问题往更小的局部压缩。这个拆分过程呈现为一棵 &lt;strong&gt;自顶向下展开的二叉树结构&lt;/strong&gt; ，每次递归都把当前序列分成两个规模更小的子序列。当序列被拆分到 &lt;strong&gt;只剩一个元素&lt;/strong&gt; 时，便不再继续。因为单个元素可以视为 &lt;strong&gt;天然有序&lt;/strong&gt; ，这也意味着递归来到了最底层。整个拆分过程的深度大约为 $logn$ ，随着递归的不断深入，原本完整的数据被切割为许多个最微小的基本单位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmerge-sort%5C%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F1.png&quot; alt=&quot;归并排序图像&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;有序合并阶段&lt;/h3&gt;
&lt;p&gt;当拆分不再继续时，算法开始 &lt;strong&gt;反向回溯&lt;/strong&gt; ，此时每一步返回都伴随着合并操作的发生。两个分别 &lt;strong&gt;已排序的子序列&lt;/strong&gt; 将被重新组合成一个更大的有序序列。合并时使用双指针技巧，从两个序列的开头开始比较大小，每次选择较小的元素加入结果中，然后将相应指针向前移动。随着比较逐渐推进，所有元素都会以 &lt;strong&gt;从小到大的顺序重新归位&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5CACM%5Cacm-note%5Cmerge-sort%5C%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F2.png&quot; alt=&quot;归并排序图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上图示例的最后一次合并中，两个长度为四的有序子序列 $[4,5,7,8]$ 和 $[1,2,3,6]$ 会依次比较并重组，最终得到全局排好序的结果 $[1,2,3,4,5,6,7,8]$ 。整个排序过程就是在这样的 “自底向上合并” 中逐渐完成的，局部的有序不断向外扩散，直到还原为完整序列。&lt;/p&gt;
&lt;p&gt;这个优雅的过程充分展示了分治思想的精髓：将复杂问题拆分为简单问题，解决后再完美组合。无论数据初始状态如何，归并排序都能以稳定且可靠的方式将其整理为正确的有序形式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体原理可以看左神的视频讲解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=531799474&amp;amp;bvid=BV1wu411p7r7&amp;amp;cid=1222566835&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;排序代码讲解&lt;/h2&gt;
&lt;p&gt;归并排序的代码实现主要由 &lt;strong&gt;递归拆分&lt;/strong&gt; 与 &lt;strong&gt;有序合并&lt;/strong&gt; 两个核心模块构成。在程序执行过程中，首先通过递归逻辑将输入数组按中点位置不断进行等分，直到子数组的长度减至 $1$ ，从而达到逻辑上的初始有序状态。随后，算法进入关键的合并阶段，通过预设的比较逻辑，将两个已排序的子序列重新组合成一个更大规模的有序集合。这种结构化的设计，使得复杂的排序任务被分解为一系列简单的双路归并操作。下面我们将针对具体的代码片段，详细分析其递归基准条件的设定、中点的计算方式以及双指针合并逻辑的具体实现细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

void merge(int l, int r, int mid, int *a){
    int b[r - l + 1]; 
    int i = l, j = mid + 1, k = 0;

    while (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= r){
        if (a[i] &amp;lt;= a[j]) b[k++] = a[i++];
        else b[k++] = a[j++];
    }
    while (i &amp;lt;= mid) b[k++] = a[i++];
    while (j &amp;lt;= r) b[k++] = a[j++];

    for (int i = 0; i &amp;lt; r - l + 1; i++){
        a[l + i] = b[i];
    }
}

void merge_sort(int l, int r, int *a){
    if (l == r) return;

    int mid = (l + r) / 2;
    merge_sort(l, mid, a);
    merge_sort(mid + 1, r, a);
    merge(l, r, mid, a);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归结构解析&lt;/h3&gt;
&lt;p&gt;在学习归并排序时，许多人虽然能够写出代码，却没有真正理解 &lt;strong&gt;递归为何能够逐层收敛&lt;/strong&gt; 。本节将从 &lt;strong&gt;区间划分的角度&lt;/strong&gt; 出发，把这一过程的 &lt;strong&gt;核心机制&lt;/strong&gt; 彻底讲清楚，使逻辑不再停留在记忆层面，而是能够真正掌握其中的含义。&lt;/p&gt;
&lt;p&gt;递归函数的关键就在 &lt;strong&gt;区间划分&lt;/strong&gt; ，而区间划分的关键就在 &lt;strong&gt;计算中点 mid&lt;/strong&gt; 上。关于 $mid$ 的计算方式可以分为 &lt;strong&gt;上取整和下取整&lt;/strong&gt; 两种方式，而上取整与下取整的差别，背后对应着 &lt;strong&gt;两套不同的区间收敛路径&lt;/strong&gt; 。一个看似简单的选择，决定了 &lt;strong&gt;子区间能否持续缩小&lt;/strong&gt; ，也直接影响 &lt;strong&gt;递归能否正确终止&lt;/strong&gt; 。接下来，我们就从 $mid$ 不同的计算方式入手，理解归并排序 &lt;strong&gt;真正合理且稳定的划分方式&lt;/strong&gt; 。&lt;/p&gt;
&lt;h4&gt;情况一：下取整&lt;/h4&gt;
&lt;p&gt;我们直接考虑极限情况，当区间已经缩小到只有两个元素，即 $r = l + 1$ 时，根据公式可以得到：&lt;/p&gt;
&lt;p&gt;$$
\displaystyle mid = \left\lfloor \frac{l + r}{2} \right\rfloor = \left\lfloor l + \frac{1}{2} \right\rfloor = l
$$&lt;/p&gt;
&lt;p&gt;这意味着当左右端点相邻时 $mid$ 会与左端点重合。为了确保区间能够继续被有效划分，我们将区间切分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左区间 $[l, mid]$&lt;/li&gt;
&lt;li&gt;右区间 $[mid + 1, r]$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样无论如何，两个区间最终都能各自缩小到一个元素。因此在代码中常见这样的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;merge_sort(l, mid, a);
merge_sort(mid + 1, r, a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们尝试将区间划分成 $[l, mid]$ 和 $[mid, r]$ ，那么在 l 与 r 相邻时，得到的区间就会变为 $[l, l]$ 和 $[l, r]$ ，我们会发现右边的区间始终都是 $[l, r]$ 从而陷入死循环。&lt;/p&gt;
&lt;p&gt;当区间已经缩小到只一个元素，即 $l == r$ 时，根据公式我们可以得到：&lt;/p&gt;
&lt;p&gt;$$
\displaystyle mid = \left\lfloor \frac{l + r}{2} \right\rfloor = \left\lfloor l \right\rfloor = l
$$&lt;/p&gt;
&lt;p&gt;如果按照之前的区间划分方式，我们会得到 $[l, l]$ 和 $[l + 1, l]$ 两个区间。左边的区间会进入无限循环状态，而右边的区间则直接进入非法状态。为了避免这种情况发生，我们需要在函数中明确阻止递归进入单元素区间，于是代码中加入了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (l == r) return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一判断确保递归在区间缩小到单个元素时能够正确终止，从而保证整个归并排序过程安全而稳定。&lt;/p&gt;
&lt;h4&gt;情况二：上取整&lt;/h4&gt;
&lt;p&gt;我们依旧考虑极限情况，当区间已经缩小到只有一个元素，即 $l = r$ 时，根据公式可以得到：&lt;/p&gt;
&lt;p&gt;$$
\displaystyle mid = \left\lceil \frac{l + r}{2} \right\rceil = \left\lceil l + \frac{1}{2} \right\rceil = l + 1 = r
$$&lt;/p&gt;
&lt;p&gt;这意味着当左右端点相邻时 $mid$ 会与右端点重合。为了确保区间能够继续被有效划分，我们将区间切分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左区间 $[l, mid - 1]$&lt;/li&gt;
&lt;li&gt;右区间 $[mid, r]$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样无论如何，两个区间最终都能各自缩小到一个元素，符合归并排序的设计预期。&lt;/p&gt;
&lt;p&gt;跟上取整的情况类似，如果我们尝试将区间划分成 $[l, mid]$ 和 $[mid, r]$ ，那么在 l 与 r 相邻时，得到的区间就会变为 $[l, r]$ 和 $[r, r]$ ，我们会发现左边的区间始终都是 $[l, r]$ 从而陷入死循环。&lt;/p&gt;
&lt;p&gt;当区间已经缩小到只一个元素，即 $l == r$ 时，根据公式我们可以得到：&lt;/p&gt;
&lt;p&gt;$$
\displaystyle mid = \left\lceil \frac{l + r}{2} \right\rceil = \left\lceil r \right\rceil = r
$$&lt;/p&gt;
&lt;p&gt;跟上取整的情况类似，我们会得到 $[r, r -  1]$ 和 $[r, r]$ 两个区间。右边的区间会进入无限循环状态，而左边的区间则直接进入非法状态，因此我们同样要禁止代码进入 $l == r$ 的状态。&lt;/p&gt;
&lt;h3&gt;合并结构解析&lt;/h3&gt;
&lt;p&gt;归并排序的核心不仅在于 &lt;strong&gt;递归拆分区间&lt;/strong&gt; ，更在于 &lt;strong&gt;如何将拆开的子区间重新合并为有序数组&lt;/strong&gt; 。理解这一点有助于我们掌握归并排序 &lt;strong&gt;分治思想的完整闭环&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在每一次递归返回时，两个子区间已经各自排好序，但整体仍然是分散的。&lt;strong&gt;合并函数的任务&lt;/strong&gt; ，就是通过比较两个子区间的元素，将它们按大小顺序依次放入一个临时数组中。具体来说，我们从两个子区间的起点开始，依次比较当前元素的大小，将较小的元素放入临时数组，并向前推进对应的指针。这个过程一直持续到其中一个子区间被完全取出为止。随后，将另一个子区间剩余的所有元素直接追加到临时数组末尾。最后，将临时数组的内容复制回原数组对应的位置，&lt;strong&gt;完成一次完整的归并&lt;/strong&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= r){
    if (a[i] &amp;lt;= a[j]) b[k++] = a[i++];
    else b[k++] = a[j++];
}
while (i &amp;lt;= mid) b[k++] = a[i++];
while (j &amp;lt;= r) b[k++] = a[j++];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种合并策略保证了两个 &lt;strong&gt;已排序的子区间&lt;/strong&gt; 能够 &lt;strong&gt;高效、稳定地&lt;/strong&gt; 组合成更大的有序区间。值得注意的是，&lt;strong&gt;临时数组的大小&lt;/strong&gt; 总是等于待合并区间的长度，而 &lt;strong&gt;每次合并的时间复杂度为 $O(n)$&lt;/strong&gt; ，其中 $n$ 为当前区间的长度。由于归并排序的 &lt;strong&gt;递归深度为 $\log n$&lt;/strong&gt; ，整体的 &lt;strong&gt;时间复杂度为 $O(n \log n)$&lt;/strong&gt; ，这也是归并排序能够保持高效的原因之一。&lt;/p&gt;
&lt;p&gt;通过理解 &lt;strong&gt;递归的拆分逻辑和合并机制&lt;/strong&gt; ，我们就能清楚地看到归并排序的整体流程：每次递归都在 &lt;strong&gt;缩小问题规模&lt;/strong&gt; ，而每次合并则在 &lt;strong&gt;逐步恢复全局顺序&lt;/strong&gt; 。正是这种 &lt;strong&gt;“分而治之” 的思想&lt;/strong&gt; ，使得归并排序既稳定又高效，并且能够处理任意长度的数组而无需依赖额外的复杂操作。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;归并分治基本原理&lt;/h1&gt;
&lt;p&gt;对于多维偏序问题，常见的处理方式是先对其中一个维度进行排序，以此固定该维度上的相对顺序，从而实现降维并简化问题。然而，这种排序手段 &lt;strong&gt;只能使用一次&lt;/strong&gt; 。在二维偏序问题中能较为从容地解决，而当维度扩展到三维及更高时，单次排序已不足以同时维护多个维度之间的偏序关系。&lt;strong&gt;归并分治正是在这一背景下引入的一种方法&lt;/strong&gt; ，它借助分治结构，在算法流程中自然地引入顺序约束，从而隐式地维护多维偏序关系。&lt;/p&gt;
&lt;p&gt;归并分治的关键在于 &lt;strong&gt;它能够在不影响统计完整性的前提下，间接实现对两个维度的排序&lt;/strong&gt; 。在分治过程中，元素被划分为左右两部分，由于划分本身建立在第一维偏序关系之上，因此在合并阶段，左半部分中的所有元素在第一维偏序上必然不大于右半部分中的元素，&lt;strong&gt;第一维的偏序关系在结构层面已经被保证&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;基于这一事实，在合并阶段可以将左右两部分视为在第一维偏序关系上有序的两个整体。只要不改变元素所属的分治区间，左右两部分内部的元素就可以任意重排而不破坏第一维偏序约束，因此可以在合并阶段进一步按照第二维偏序进行排序或扫描，&lt;strong&gt;这正是归并分治能够同时处理两维偏序关系的原因&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在统计贡献时，归并分治依赖方向性的约束。如果在合并阶段仅计算左侧元素对右侧元素的贡献，则对于任意一个元素而言，其最终累计的正是所有位于其左侧的元素对自身产生的影响。由于任意一对满足 “左在前、右在后” 的元素，&lt;strong&gt;必然会在某一层分治中恰好被统计一次&lt;/strong&gt; ，因此不会遗漏或重复。&lt;/p&gt;
&lt;h2&gt;陈丹琦分治简介&lt;/h2&gt;
&lt;p&gt;CDQ 分治（由陈丹琦在 2008 年国际信息学奥林匹克中国国家队选拔赛中正式提出）&lt;strong&gt;本质上是归并分治思想在处理多维偏序问题中的深度应用&lt;/strong&gt; 。它并未在分治的基础框架上引入全新的逻辑，而是充分利用了归并排序在合并阶段天然具备的 “左侧先于右侧” 的顺序约束。这种约束使得我们能够在不依赖对所有维度进行物理排序的前提下，建立起维度之间的逻辑依赖，从而完成多维偏序关系的精准统计。因此，CDQ 分治与其说是一种独立于归并分治之外的全新算法，倒不如说是一种高度抽象的 &lt;strong&gt;问题建模方式与统计框架&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在实际算法竞赛与中，CDQ 分治常用于解决 &lt;strong&gt;三维及以上的高维偏序类问题&lt;/strong&gt; 。这类问题的核心挑战在于多个维度之间相互交织的约束关系，传统的单维排序往往顾此失彼。而 CDQ 分治通过 &lt;strong&gt;一维排序、二维归并、三维统计&lt;/strong&gt; 的经典套路，将复杂问题逐层降维：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一维排序&lt;/strong&gt;：通过对第一维进行全局预处理排序，确保在后续的分治过程中，左区间的任何元素在第一维上都天然满足小于等于右区间元素的条件，从而消除了第一维的偏序限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二维归并&lt;/strong&gt;：在合并过程中，程序通过归并排序的方式对第二维进行重排，这使得我们在处理右区间的每个元素时，可以利用双指针将左区间中满足第二维限制的元素逐一加入统计范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三维统计&lt;/strong&gt;：由于前两维的约束已经在分治与双指针的过程中被逻辑化简，此时只需用 &lt;strong&gt;树状数组或线段树&lt;/strong&gt; 等高效数据结构维护即可在 $O(\log n)$ 的时间内完成实时统计。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在实际应用中，这种分治架构为处理 &lt;strong&gt;大规模数据下的多维依赖&lt;/strong&gt; 提供了极其稳健的底层支撑。它将原本需要嵌套多层高级数据结构才能解决的复杂计数，转化为了一系列在线性扫描过程中完成的单向更新。这种降维策略不仅显著降低了算法的实现难度，更通过规避复杂的平衡树等结构，极大地减小了程序的常数开销与内存占用，使其成为处理各类偏序统计与几何计数问题的通用范式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体的题目类型可以看下面这些视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;&quot;&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114607746195424&amp;amp;bvid=BV1sa7zz1EVx&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114653598322831&amp;amp;bvid=BV1RsTiz4EAw&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;此外，CDQ 分治在 &lt;strong&gt;动态问题静态化&lt;/strong&gt; 方面也展现出极强的普适性。通过引入时间轴作为第一维坐标，我们可以将复杂的动态修改与实时查询操作，统一建模为静态的三维偏序问题。在这种视角下，每一次修改被视为一个带有时间戳的贡献点，而每一次查询则是在特定时间窗口与空间范围内的权值汇总。这种建模方式不仅规避了编写复杂动态结构，更通过稳定的归并结构提升了代码的执行效率。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;偏序数对题目收集&lt;/h1&gt;
&lt;p&gt;在多维偏序的研究体系中，绝大部分核心逻辑均可归结为 &lt;strong&gt;偏序数对的统计与计数问题&lt;/strong&gt; 。该类问题的形式化定义是在给定点集中，检索并计数所有满足各维度偏序约束的元素对。例如在 $k$ 维空间中，算法需高效统计满足 $\forall d \in {1, \dots, k}, x_{i,d} \leq x_{j,d}$ 条件的点对 $(i, j)$ 。由于偏序关系不具备全序结构的线性特征，直接进行 $O(N^2)$ 的暴力枚举会导致计算复杂度过高，因此通常需要引入 &lt;strong&gt;结构化维护&lt;/strong&gt; 技巧来实现复杂度的优化。&lt;/p&gt;
&lt;p&gt;从建模视角分析，&lt;strong&gt;二维数点问题&lt;/strong&gt; 是偏序数对统计最经典的工程实践。通过容斥原理将矩形区域查询拆解为前缀矩形查询后，问题被转化为统计满足 $x_i \leq X$ 且 $y_i \leq Y$ 的元素集合。这在坐标系中本质上是检索与给定参考点 $(X, Y)$ 构成 &lt;strong&gt;二维偏序关系&lt;/strong&gt; 的点集。这种从空间几何区域到偏序约束关系的抽象，揭示了多维坐标约束的代数本质，并为利用扫描线算法配合树状数组或线段树等动态维护结构提供了逻辑前提。&lt;/p&gt;
&lt;p&gt;在算法竞赛中，偏序数对问题具有极强的代表性与普适性。它们不仅是多维偏序建模的最直接表现形式，能够清晰刻画不同维度约束间的耦合作用，且许多表面迥异的问题（如逆序对统计、动态区间和、以及复杂的几何覆盖问题）在剥离表面逻辑后，均可统一纳入 &lt;strong&gt;偏序数对统计&lt;/strong&gt; 的数学框架内。因此，对此类题目进行系统性整理与分类，有助于建立起针对多维偏序问题的通用认知结构。&lt;/p&gt;
&lt;h2&gt;计算数组的小和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nowcoder.com/practice/edfe05a1d45c4ea89101d936cac32469&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;数组小和的定义：$\displaystyle \sum_{i=1}^{n}f_i$ ，其中 $f_i$ 的定义是第 $i$ 个数左侧小于等于 $s_i$ 的个数。&lt;/p&gt;
&lt;p&gt;例如，数组 $s = [1, 3, 5, 2, 4, 6]$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 $s[0]$ 的左边小于或等于 $s[0]$ 的数的和为 $0$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 $s[1]$ 的左边小于或等于 $s[1]$ 的数的和为 $1$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 $s[2]$ 的左边小于或等于 $s[2]$ 的数的和为 $1 + 3 = 4$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 $s[3]$ 的左边小于或等于 $s[3]$ 的数的和为 $1$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 $s[4]$ 的左边小于或等于 $s[4]$ 的数的和为 $1 + 3 + 2 = 6$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 $s[5]$ 的左边小于或等于 $s[5]$ 的数的和为 $1 + 3 + 5 + 2 + 4 = 15$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 $s$ 的小和为 $0 + 1 + 4 + 1 + 6 + 15 = 27$&lt;/p&gt;
&lt;p&gt;给定一个数组 $s$  ，实现函数返回 $s$ 的小和。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$0 &amp;lt; n \leq 10^5$&lt;/li&gt;
&lt;li&gt;$-100 \leq s[i] \leq 100$&lt;/li&gt;
&lt;li&gt;所有输入均为整数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$s_1 \quad s_2 \quad \ldots \quad s_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示数组的小和。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6
1 3 5 2 4 6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;27
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;理解该问题的关键在于转换统计视角：与其计算每个数左侧有多少个比它小的数，不如统计每个数 $s_i$ 会作为较小值被其右侧多少个比它大的数累加。这一逻辑 &lt;strong&gt;与经典算法 “逆序对” 高度对称&lt;/strong&gt; ，只不过逆序对统计的是 “左侧比自己大的个数” ，而本题则是在归并排序的合并阶段，利用左右子区间的有序性，一次性计算出左区间的数对右区间所有更大元素的贡献。&lt;/p&gt;
&lt;p&gt;这种基于归并分治的策略，在 $O(n \log n)$ 的排序过程中顺带完成了跨区间的数值累加。由于每个数对的贡献在合并时被精确统计且不重复，该方法不仅在时间复杂度上远优于 $O(n^2)$ 的暴力扫描，其实现逻辑也比树状数组或线段树等结构更加直观。通过这种分而治之的思想，我们将复杂的全局统计拆解为局部有序区间之间的线性累加，完美契合了序列统计类问题的优化本质。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 100;
int n; ll ans = 0;
int a[MAXN];

void merge(int l, int r, int mid, int *a){
    int b[r - l + 1];
    int i = l, j = mid + 1, k = 0;

    while (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= r){
        if (a[i] &amp;lt;= a[j]){
            // a[i] 对右侧 [j, r] 的所有元素产生贡献
            ans += 1LL * a[i] * (r - j + 1);
            b[k++] = a[i++];
        } else {
            b[k++] = a[j++];
        }
    }

    while (i &amp;lt;= mid) b[k++] = a[i++];
    while (j &amp;lt;= r) b[k++] = a[j++];

    for (int t = 0; t &amp;lt; k; t++){
        a[l + t] = b[t];
    }
}

void merge_sort(int l, int r, int *a){
    if (l == r) return;

    int mid = (l + r) / 2;
    merge_sort(l, mid, a);
    merge_sort(mid + 1, r, a);
    merge(l, r, mid, a);
}

int main(){
    cin &amp;gt;&amp;gt; n;
    for (int i = 0; i &amp;lt; n; i++){
        cin &amp;gt;&amp;gt; a[i];
    }

    if (n &amp;gt; 1){
        merge_sort(0, n - 1, a);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;统计重要翻转对&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-pairs/description/&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;给定一个数组 $nums$ ，如果 $i &amp;lt; j$ 且 $nums[i] &amp;gt; 2 * nums[j]$ 我们就将 $(i, j)$ 称作一个 &lt;strong&gt;重要翻转对&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;你需要返回给定数组中的重要翻转对的数量。&lt;/p&gt;
&lt;h3&gt;Constraints&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;给定数组的长度不会超过 $50000$&lt;/li&gt;
&lt;li&gt;输入数组中的所有数字都在 $32$ 位整数的表示范围内&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Input&lt;/h3&gt;
&lt;p&gt;输入包含两行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一行包含一个整数 $N$ ，表示数组的长度。&lt;/li&gt;
&lt;li&gt;第二行包含 $N$ 个整数，表示数组中的各个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$N$&lt;/p&gt;
&lt;p&gt;$nums_1 \quad nums_2 \quad \ldots \quad nums_N$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Output&lt;/h3&gt;
&lt;p&gt;输出一个整数表示数组翻转对的数量。&lt;/p&gt;
&lt;h3&gt;Sample Input 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
1 3 2 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 1&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Input 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5
2 4 3 5 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sample Output 2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目要点解析&lt;/h2&gt;
&lt;p&gt;解决该问题的关键在于利用归并排序中左右子区间的 &lt;strong&gt;天然位置顺序&lt;/strong&gt; 。在分治的合并阶段，左区间的所有元素下标 $i$ 必然小于右区间的所有元素下标 $j$ 。由于左右子区间在统计前已各自有序，我们可以通过双指针扫描：对于右区间的每一个 $nums[j]$ ，在左区间中找到第一个满足 $nums[i] &amp;gt; 2 \cdot nums[j]$ 的位置。一旦找到，根据有序性，左区间从 $i$ 到末尾的所有元素都必然满足该条件，从而实现 $O(n \log n)$ 的高效计数。&lt;/p&gt;
&lt;p&gt;需要注意的是，本题与普通逆序对或数组小和问题略有不同，因为比较条件 $nums[i] &amp;gt; 2 \cdot nums[j]$ 具有 &lt;strong&gt;非对称性&lt;/strong&gt; 。这意味着我们不能在归并过程中顺便计数，必须 &lt;strong&gt;在此之前&lt;/strong&gt; 用一个独立的逻辑完成该层级的翻转对统计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const int MAXN = 5e4 + 100;
int n; ll ans = 0;
ll a[MAXN];

void merge(int l, int r, int mid, ll *a){
    // 统计重要翻转对
    int j = mid + 1;
    for (int i = l; i &amp;lt;= mid; i++){
        while (j &amp;lt;= r &amp;amp;&amp;amp; a[i] &amp;gt; 2LL * a[j]){
            j++;
        }
        ans += (j - (mid + 1));
    }

    // 正常归并排序
    ll b[r - l + 1];
    int i = l; j = mid + 1; int k = 0;

    while (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= r){
        if (a[i] &amp;lt;= a[j]) b[k++] = a[i++];
        else b[k++] = a[j++];
    }

    while (i &amp;lt;= mid) b[k++] = a[i++];
    while (j &amp;lt;= r) b[k++] = a[j++];

    for (int t = 0; t &amp;lt; k; t++){
        a[l + t] = b[t];
    }
}

void merge_sort(int l, int r, ll *a){
    if (l &amp;gt;= r) return;

    int mid = (l + r) / 2;
    merge_sort(l, mid, a);
    merge_sort(mid + 1, r, a);
    merge(l, r, mid, a);
}

int main(){
    cin &amp;gt;&amp;gt; n;
    for (int i = 0; i &amp;lt; n; i++){
        cin &amp;gt;&amp;gt; a[i];
    }

    if (n &amp;gt; 1){
        merge_sort(0, n - 1, a);
    }

    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; &quot;\n&quot;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/basic/merge-sort/&quot;&gt;【OI WiKi】归并排序相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/justidle/article/details/104203958&quot;&gt;【CSDN 博客】归并排序介绍&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/misc/cdq-divide/&quot;&gt;【OI WiKi】CDQ 分治相关知识&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/article/nl6r7elc&quot;&gt;【Luogu 博客】CDQ 分治和整体二分&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_30320171/article/details/129787418&quot;&gt;【CSDN 博客】二维数点/二维偏序&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Cat-litter/articles/19243793&quot;&gt;【Add_Catalyst】偏序问题专题合集&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基础算法】第三节：近似推断算法</title><link>https://xingguang641.com/posts/approximate-inference/approximate-inference/</link><guid isPermaLink="true">https://xingguang641.com/posts/approximate-inference/approximate-inference/</guid><description>介绍机器学习常见的算法</description><pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;近似推断算法背景介绍&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;近似推断（Approximate Inference）&lt;/strong&gt; 算法是在概率模型中用于计算难以直接求解的后验概率分布、边缘概率分布或函数期望等推断任务的一类方法。&lt;/p&gt;
&lt;p&gt;在许多复杂的模型中（例如具有高维隐变量的贝叶斯模型或深度学习模型），精确推断（Exact Inference）往往因为涉及到复杂的积分或求和运算而 &lt;strong&gt;计算代价高昂&lt;/strong&gt; ，甚至是 &lt;strong&gt;难以计算&lt;/strong&gt; 。近似推断的目的就是在 &lt;strong&gt;计算精度和计算资源之间进行权衡&lt;/strong&gt; ，以便在有限的时间内获得一个足够好的近似解。&lt;/p&gt;
&lt;h2&gt;近似推断概览（Overview）&lt;/h2&gt;
&lt;p&gt;近似推断方法主要分为两大类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;随机/采样方法（Stochastic/Sampling Methods）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：通过 &lt;strong&gt;大量采样&lt;/strong&gt; 来近似目标分布（通常是真实的后验分布）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代表算法&lt;/strong&gt;：&lt;strong&gt;马尔可夫链蒙特卡洛 (MCMC)&lt;/strong&gt; 方法，如 Gibbs 采样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：理论上，随着采样数量的增加，可以得到 &lt;strong&gt;更精确&lt;/strong&gt; 的近似结果，但收敛速度可能较慢，且难以判断何时收敛。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;确定性近似方法（Deterministic Approximation Methods）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：将推断问题转化为一个 &lt;strong&gt;优化问题&lt;/strong&gt; ，通过寻找一个 &lt;strong&gt;形式简单、易于处理的近似分布&lt;/strong&gt; $Q$ 来逼近真实的后验分布 $P$ 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代表算法&lt;/strong&gt;：&lt;strong&gt;变分推断&lt;/strong&gt;（Variational Inference，简称 VI），特别是变分贝叶斯推断（Variational Bayesian Inference）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：通常具有 &lt;strong&gt;解析解&lt;/strong&gt;（或可通过迭代优化求解），&lt;strong&gt;计算开销小、速度快&lt;/strong&gt; ，易于应用于大规模问题，但其近似能力受限于所选近似分布 $Q$ 的形式（例如平均场假设）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总的来说&lt;/strong&gt;： 当精确推断不可行时，近似推断提供了一种实用的解决方案，它通过随机采样或构造优化目标（如变分推断中的变分下界）来高效地估计复杂的概率分布。&lt;/p&gt;
&lt;p&gt;接下来我们就按顺序讲解马尔科夫蒙特卡洛、重要性采样、变分推断和期望传播四个算法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;马尔可夫蒙特卡洛基本原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;马尔可夫蒙特卡洛方法&lt;/strong&gt;（Markov Chain Monte Carlo，简称 MCMC）是一类用于从复杂概率分布中抽样的算法。它的提出源于一个普遍而棘手的问题：当一个概率分布无法直接采样，且归一化常数难以计算时，我们仍希望获得来自该分布的代表性样本，以便进行积分估计、贝叶斯推断和模型分析。&lt;/p&gt;
&lt;p&gt;MCMC 的核心价值就在于：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;它提供了一套统一的框架，使我们能够在高维、非解析、结构复杂的概率空间中有效地获取样本。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在许多统计或贝叶斯推断的任务中，我们面对的分布常常具有如下形式：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \frac{1}{Z} \bar{P}(x)
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{P}$ 可以直接计算（例如来自似然与先验的乘积），但归一化常数 $\displaystyle Z = \int \bar{P}(x) , dx$ 在高维空间中往往既无法解析求解，也难以通过常规数值方法逼近。特别是当模型结构复杂、维度极高时，积分的计算几乎是不可能完成的任务。&lt;/p&gt;
&lt;p&gt;然而在许多的任务中，如贝叶斯后验分析、期望计算、边缘化、模型比较，都需要依赖对 $P(x)$ 的可操作采样。如果缺少样本，我们就无法继续进行统计推断。&lt;/p&gt;
&lt;p&gt;因此我们迫切需要一种方法，能够在不知道 $Z$ 的前提下，从 $P(x)$ 中生成近似样本。MCMC 正是针对这一难题提出的：它不要求直接采样，而是构造一个特殊的随机过程，其长期行为自然会收敛到目标分布。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Capproximate-inference%5C%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E8%92%99%E7%89%B9%E5%8D%A1%E6%B4%9B1.jpg&quot; alt=&quot;马尔可夫蒙特卡洛图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;蒙特卡洛方法（Monte Carlo Method）&lt;/h2&gt;
&lt;p&gt;蒙特卡洛方法是现代统计计算和高维概率推断中不可或缺的工具。它的核心思想在于，通过随机抽样来逼近期望、积分和其他复杂统计量，而这种逼近在数学上有严格的收敛保证。其特别之处在于，它的误差行为与空间维度几乎无关，这使得它在传统数值方法难以处理的高维问题中仍然可行。&lt;/p&gt;
&lt;h3&gt;随机抽样&lt;/h3&gt;
&lt;p&gt;蒙特卡洛方法建立在一个简单而深刻的观察之上：对一个目标分布 $P(x)$ 的期望可以通过从该分布中抽取样本并取平均来近似。&lt;/p&gt;
&lt;p&gt;如果能够获得独立同分布样本 $x^{(1)}, x^{(2)}, \ldots, x^{(N)} \sim P(x)$ ，则样本均值可以用下面这个公式估计：&lt;/p&gt;
&lt;p&gt;$$
\hat{\mu}&lt;em&gt;N = \frac{1}{N} \sum&lt;/em&gt;{i=1}^{N} f(x^{(i)})
$$&lt;/p&gt;
&lt;p&gt;随着样本数量 $N$ 的增加，将逐渐接近期望 $\mathbb{E}_P \Big[ f(X) \Big]$ 。这种方法的直观理解是，每个样本都携带了目标分布的信息，多个样本的平均自然会反映整体分布的统计特性。&lt;/p&gt;
&lt;p&gt;更重要的是，这一思路不仅适用于简单的低维分布，即使在数十维、数百维的空间中也同样有效。传统数值积分方法，如梯形法、Simpson 法或者高斯求积，误差会随着维度呈指数级增长，导致所谓的 “维数灾难” 。蒙特卡洛方法通过随机化抽样，避开了空间划分的维度限制，因此成为高维概率计算的唯一可行途径之一。&lt;/p&gt;
&lt;h3&gt;收敛性证明&lt;/h3&gt;
&lt;p&gt;蒙特卡洛方法的有效性依赖于概率论中的大数定律和中心极限定理。大数定律保证了样本均值在概率意义下会收敛到真实期望，即：&lt;/p&gt;
&lt;p&gt;$$
\hat{\mu}_N \xrightarrow{P} \mathbb{E}_P \Big[ f(X) \Big] \quad (N \to \infty)
$$&lt;/p&gt;
&lt;p&gt;意味着随着样本数量不断增加，蒙特卡洛估计几乎必然逼近真实值。&lt;/p&gt;
&lt;p&gt;中心极限定理进一步说明了估计误差的分布性质：&lt;/p&gt;
&lt;p&gt;$$
\sqrt{N} \Big( \hat{\mu}_N - \mathbb{E} \big[ f(X) \big] \Big) \xrightarrow{d} \mathcal{N}(0, \sigma^2)
$$&lt;/p&gt;
&lt;p&gt;其中 $\sigma^2$ 是 $f(X)$ 在分布 $P(x)$ 下的方差。这表明蒙特卡洛估计的标准误差随样本数量 $N$ 下降的速率为 $1/\sqrt{N}$ ，且与空间维度无关。即便是在几百维或上千维的空间，这一收敛性质仍然成立，这也是蒙特卡洛方法在高维问题中表现优异的根本原因。&lt;/p&gt;
&lt;p&gt;此外，中心极限定理还提供了误差的概率描述，使得我们能够给出置信区间和统计显著性分析，从而不仅能够得到近似值，还能量化估计的不确定性。这一特性在贝叶斯推断和复杂模型分析中尤为重要。&lt;/p&gt;
&lt;h3&gt;采样难题&lt;/h3&gt;
&lt;p&gt;尽管蒙特卡洛方法本身非常简洁，但在实际应用中，其真正的难点并不在于如何取平均，而在于如何获得样本。在许多问题中，尤其是贝叶斯后验分析，我们面对的是非规范化密度 $\bar{P}(x)$ ，而完整的概率密度则需要用到归一化常数 $\displaystyle Z = \int \bar{P}(x) , dx$ 。当模型复杂或维度很高时，这个积分无法解析求解，也难以通过数值方法近似。&lt;/p&gt;
&lt;p&gt;大多数传统抽样方法，如逆变换采样或拒绝采样，都依赖于完整的概率密度，因此在无法获得归一化常数 $Z$ 时就无法使用。更直观地说，如果我们想用蒙特卡洛方法估计期望，就必须能够直接从分布中抽样，而直接抽样的前提是知道完整的分布，包括归一化常数。而在高维复杂模型中，这个常数往往不可计算。这种矛盾正是蒙特卡洛方法在复杂应用中面临的核心瓶颈，也是 MCMC 出现的直接原因。&lt;/p&gt;
&lt;h2&gt;马尔可夫链（Markov Chain）&lt;/h2&gt;
&lt;p&gt;面对采样难题，MCMC 的核心策略是不直接抽样，而是构造一个随机过程，使其长期分布等于目标分布。马尔可夫链提供了这一策略的数学基础。理解其马尔可夫性、平稳分布以及收敛条件，是掌握 MCMC 的关键。&lt;/p&gt;
&lt;h3&gt;马尔可夫性&lt;/h3&gt;
&lt;p&gt;马尔可夫链的核心假设是 “无记忆性” 。如果随机过程 ${X_t}$ 满足：&lt;/p&gt;
&lt;p&gt;$$
P(X_{t+1} = x&apos; | X_t = x, X_{t-1}, \ldots, X_0) = P(X_{t+1} = x&apos; | X_t = x)
$$&lt;/p&gt;
&lt;p&gt;则称其具有 &lt;strong&gt;马尔可夫性&lt;/strong&gt; 。即未来的演化仅依赖当前状态，而与更早的历史无关。&lt;/p&gt;
&lt;p&gt;转移方式由 &lt;strong&gt;转移核&lt;/strong&gt; 描述：&lt;/p&gt;
&lt;p&gt;$$
T(x \rightarrow x&apos;) = P(X_{t+1} = x&apos; | X_t = x)
$$&lt;/p&gt;
&lt;p&gt;MCMC 的设计任务就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如何构造一个转移核，使目标分布成为该马尔可夫链的长期分布？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;平稳分布&lt;/h3&gt;
&lt;p&gt;若分布 $\pi$ 满足：&lt;/p&gt;
&lt;p&gt;$$
\pi(x&apos;) = \int \pi(x) , T(x \rightarrow x&apos;) , dx
$$&lt;/p&gt;
&lt;p&gt;则称其为链的 &lt;strong&gt;平稳分布（stationary distribution）&lt;/strong&gt;。直观上，如果系统当前的状态分布为 $\pi$ ，经过一步随机转移后仍然保持为 $\pi$ ，那么 $\pi$ 就描述了链在 “平衡状态” 下的统计特性。&lt;/p&gt;
&lt;p&gt;MCMC 的基本目标，就是构造一个马尔可夫链，使目标分布 $P(x)$ 成为它的平稳分布。&lt;/p&gt;
&lt;p&gt;然而，仅仅存在一个平稳分布式不够的，我们还需要保证链能够 “走到” 这个分布，并且不会陷入震荡或局部区域。因此，链收敛到平稳分布还必须满足若干条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不可约性：链必须能够遍历整个空间&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不可约性要求链从任意状态都能以正概率在有限步内到达任意其他状态：&lt;/p&gt;
&lt;p&gt;$$
T^t(x \rightarrow x&apos;) &amp;gt; 0 \quad \text{for some } t &amp;gt; 0
$$&lt;/p&gt;
&lt;p&gt;如果链不可约，它会被分成互不连通的子区域，不同区域会有不同的稳定行为，收敛结果将依赖初始点。在 MCMC 中，这意味着样本只反映局部结构，而不能代表整个目标分布。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非周期性：避免周期性震荡&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;链还必须是非周期的。对任意状态 $x$ ，其可能返回步数集合的最大公约数必须为 1：&lt;/p&gt;
&lt;p&gt;$$
\gcd{t &amp;gt; 0 : T^t(x \rightarrow x) &amp;gt; 0} = 1
$$&lt;/p&gt;
&lt;p&gt;若链具有周期性，每隔固定步数才可能回到某些状态，那么链不会真正稳定，而是在若干分布之间循环。这会破坏收敛分析，使样本统计量不可靠。&lt;/p&gt;
&lt;p&gt;现代 MCMC 算法（如 Metropolis–Hastings）通常允许当前状态 “保持在原地” ，从而打破周期性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;遍历性：确保经验统计量收敛&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当链同时具有不可约性与非周期性，并满足一定的正则条件时，就会具有遍历性（ergodicity）。遍历性意味着链的时间平均能收敛到平稳分布下的期望：&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{N} \sum_{t=1}^{N} f(X_t) \xrightarrow[N \to \infty]{} \mathbb{E}_{\pi} \Big[ f(X) \Big]
$$&lt;/p&gt;
&lt;p&gt;遍历性是 MCMC 的根本理论保障：它确保我们对样本求平均、求边缘概率、进行积分估计时，能够得到与目标分布一致的结果。&lt;/p&gt;
&lt;p&gt;换言之，没有遍历性，所有基于 MCMC 的统计量都可能失效。这为 MCMC 的一切统计操作提供理论基础。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;不知道你是否想过：当我们只能计算一个分布的未归一化密度，却无法直接从它生成样本时，我们究竟能做些什么？从推断的角度看，只要能独立地从后验里抽到样本，后续的估计与分析几乎都会迎刃而解。但现实情况是，我们往往只能得到 $\bar{P}(x)$ ，既不能算出归一化常数，也不能直接进行采样。&lt;/p&gt;
&lt;p&gt;于是，一个最自然的想法就是：能不能利用某种更直接的随机过程来复现目标分布的形状？比如，先从一个容易采样的分布中提点，再依据密度比例筛选出符合要求的样本。&lt;strong&gt;接收拒绝采样&lt;/strong&gt; 正是基于这种朴素而直接的构造，它的结构非常清晰，也因此成为许多抽样方法的出发点。&lt;/p&gt;
&lt;h3&gt;接收拒绝采样&lt;/h3&gt;
&lt;p&gt;接收拒绝采样的核心思想很直观：既然我们无法直接从目标分布 $P(x)$ 抽样，那就找一个容易采样的分布 $Q(x)$ 来 “辅助” ，前提是它能够吧目标分布整体覆盖住。也就是说，存在一个常数 $M &amp;gt; 0$ ，使得堆所有 $x$ 有：&lt;/p&gt;
&lt;p&gt;$$
\bar{P}(x) \geq MQ(x)
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{P}(x)$ 是目标分布的未归一化密度。&lt;/p&gt;
&lt;p&gt;操作过程就非常简单：首先从 $Q(x)$ 中生成一个候选样本 $x^*$ ，然后生成一个均匀随机数 $u \sim \text{Uniform}(0,1)$ ，如果满足下面这个条件我们就选择接收，否则选择拒绝。&lt;/p&gt;
&lt;p&gt;$$
u \leq \frac{\bar{P}(x^&lt;em&gt;)}{M Q(x^&lt;/em&gt;)}
$$&lt;/p&gt;
&lt;p&gt;重复这一过程，最终被接受的样本就服从目标分布 $P(x)$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Capproximate-inference%5C%E6%8E%A5%E6%94%B6%E6%8B%92%E7%BB%9D%E9%87%87%E6%A0%B71.jpg&quot; alt=&quot;接收拒绝采样图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个方法的优点在于概念清晰、实现简单，而且不需要知道归一化常数 $Z$（因为 $Z$ 已经被包含在常数 $M$ 中），只要能计算未归一化密度就可以运行。它在低维、规则的分布上效果很好，常常作为教学例子展示采样思想。&lt;/p&gt;
&lt;p&gt;但它的局限也很明显。首先，需要找到一个能完全覆盖目标分布的包络 $MQ(x)$ ，在复杂或者高维分布中几乎不可能做到。稍微保守一点，常数 $M$ 就会非常大，导致接受率极低。其次，高维空间里大部分体积集中在远离高密度区域的 “壳” 上，宽泛的包络分布会让绝大多数样本落在密度几乎为零的区域，从而被拒绝。这种拒绝率随着维度增加呈指数上升，使算法在高维问题中几乎无法使用。&lt;/p&gt;
&lt;p&gt;虽然接收拒绝采样直观且优雅，但在实际高维或复杂后验中并不可行。因此我们需要更加强大的采样方法来解决这个问题。&lt;/p&gt;
&lt;p&gt;通过上面的讲解，我们了解到蒙特卡洛方法是一个非常强大的算法框架，它能在高维空间中逼近期望和积分，且收敛性质良好。然而，蒙特卡洛方法真正的难点在于：&lt;strong&gt;如何从目标分布中获得样本&lt;/strong&gt; 。在复杂或高维的后验分布中，我们通常无法直接抽取独立样本，也无法计算归一化常数 $Z$，这使得传统蒙特卡洛方法难以直接应用。&lt;/p&gt;
&lt;p&gt;这正是引入马尔可夫链思想的契机。我们不再追求每个样本的独立性，而是通过构造一个 &lt;strong&gt;依赖于当前状态的随机过程&lt;/strong&gt; ，让样本沿着马尔可夫链逐步演化。如果设计得当，这条链的长期平稳分布会正好等于目标分布。也就是说，即便样本不是独立的，它们的 &lt;strong&gt;时间平均&lt;/strong&gt; 仍然可以用来近似目标分布下的期望值，从而实现蒙特卡洛估计。&lt;/p&gt;
&lt;p&gt;这种方法的关键优势在于，它不依赖归一化常数 $Z$ ，也无需构造覆盖整个高维空间的包络分布，而是通过局部移动和接受策略，逐步探索目标分布的高概率区域。这正是 MCMC 能在高维复杂模型下仍然有效的核心原因。&lt;/p&gt;
&lt;h3&gt;MCMC 流程介绍&lt;/h3&gt;
&lt;p&gt;马尔可夫蒙特卡洛方法的核心是通过构造一个马尔可夫链，让样本沿着链逐步演化，最终近似目标分布。整个过程可以抽象成一套高层次框架，每一步都有明确的目标：初始化状态、生成候选、判断接受、收集样本。具体的候选生成策略和接受规则则由具体算法（如 Metropolis-Hastings 或 Gibbs 采样）来实现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;状态初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MCMC 的迭代从一个初始状态 $X_0 = x_0$ 开始。这个初始状态可以随机生成，也可以结合先验知识选择。尽管初始状态本身不必完美靠近目标分布，但它会影响链的收敛速度以及前期样本的代表性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;候选状态生成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在每一步迭代中，当前状态 $X_t$ 用来生成下一个候选状态 $X^*$ 。候选状态的生成方式可以灵活选择，这正是 MCMC 算法的核心设计点。具体的生成策略可以参考下面讲解的 Metropolis-Hastings 或 Gibbs 采样方法。&lt;/p&gt;
&lt;p&gt;一般来说，我们希望候选状态能探索整个状态空间，同时偏向高概率区域，从而保证马尔可夫链长期分布收敛到目标分布 $P(x)$ 。在数学上，可以用转移核 $T(x_t \rightarrow X^*)$ 来描述这一过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;接收拒绝策略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;生成候选状态后，需要判断是否接受它。不同 MCMC 算法有不同的接受规则，但共同目标都是保证链的平稳分布为目标分布。被接受的候选状态成为下一步的实际状态 $X_{t+1}$ ，而被拒绝时，链则保持在当前状态 $X_t$ 。这种机制允许链以概率形式探索高概率区域，同时偶尔跳出局部极值，增强全局探索能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;样本收集与估计&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;随着迭代的进行，每一步的状态都会被记录下来，形成样本序列：&lt;/p&gt;
&lt;p&gt;$$
{X_1, X_2, \ldots, X_N }
$$&lt;/p&gt;
&lt;p&gt;在实际应用中，通常会丢弃前期的若干样本作为 &lt;strong&gt;Burn-in&lt;/strong&gt; ，以减少初始状态对后续统计量的影响。收集到的样本可以用于估计目标分布下的期望或边缘分布。例如，对于某个函数 $f(x)$ 的期望，可以通过样本均值近似：&lt;/p&gt;
&lt;p&gt;$$
\hat{\mathbb{E}} \Big[ f(X) \Big] = \frac{1}{N} \sum_{t=1}^{N} f(X_t)
$$&lt;/p&gt;
&lt;p&gt;根据马尔可夫链的遍历性（ergodicity）理论，当迭代次数 $N \rightarrow \infty$ 时，样本均值会收敛到真实的期望值：&lt;/p&gt;
&lt;p&gt;$$
\hat{\mathbb{E}} \Big[ f(X) \Big] \longrightarrow \mathbb{E}_P \Big[ f(X) \Big]
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是 MCMC 实现蒙特卡洛估计的核心机制，即使样本之间不完全独立，&lt;strong&gt;时间平均&lt;/strong&gt; 也能逼近目标分布下的期望。&lt;/p&gt;
&lt;h3&gt;Metropolis-Hastings 算法&lt;/h3&gt;
&lt;p&gt;Metropolis-Hastings（MH）是 MCMC 中最经典的算法之一，它提供了一种通用的候选生成和接受策略，能够处理任意可计算未归一化密度的分布。&lt;/p&gt;
&lt;p&gt;MH 的核心在于，从当前状态 $X_t$ 生成候选状态 $X^&lt;em&gt;$ 后，并不直接将其作为下一个状态，而是按照一个接受概率 $\alpha(X_t, X^&lt;/em&gt;)$ 来决定是否接受：&lt;/p&gt;
&lt;p&gt;$$
X_{t+1} =
\begin{cases}
X^&lt;em&gt;, &amp;amp; \text{以概率 } \alpha(X_t, X^&lt;/em&gt;) \
X_t, &amp;amp; \text{以概率 } 1 - \alpha(X_t, X^*)
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;接受概率定义为：&lt;/p&gt;
&lt;p&gt;$$
\alpha(X_t, X^&lt;em&gt;) = \min \left(1, \frac{\bar{P}(X^&lt;/em&gt;) , Q(X_t | X^&lt;em&gt;)}{\bar{P}(X_t) , Q(X^&lt;/em&gt; | X_t)}\right)
$$&lt;/p&gt;
&lt;p&gt;其中 $Q(X^* | X_t)$ 是候选状态的生成分布，可以是对称的（如正态随机游走）或非对称的。&lt;/p&gt;
&lt;p&gt;MH 的优点是非常通用，只要能计算目标分布未归一化密度 $\bar{P}(x)$ 就可以使用。缺点是对候选分布的选择较为敏感，步长太大或太小都会影响收敛速度。&lt;/p&gt;
&lt;h3&gt;Gibbs 采样算法&lt;/h3&gt;
&lt;p&gt;Gibbs 采样是 Metropolis-Hastings 的一个特例，适用于多维目标分布的情况。它通过 &lt;strong&gt;条件分布&lt;/strong&gt; 来生成每个维度的候选状态，因此无需手动设定接受概率。&lt;/p&gt;
&lt;p&gt;假设目标分布为 $P(x_1, x_2, \ldots, x_d)$ ，Gibbs 采样按坐标维度依次更新每个变量：&lt;/p&gt;
&lt;p&gt;$$
\begin{gathered}
x_1^{(t+1)} \sim P(x_1 \mid x_2^{(t)}, \dots, x_d^{(t)}) \
x_2^{(t+1)} \sim P(x_2 \mid x_1^{(t+1)}, x_3^{(t)}, \dots, x_d^{(t)}) \
\vdots \
x_d^{(t+1)} \sim P(x_d \mid x_1^{(t+1)}, \dots, x_{d-1}^{(t+1)})
\end{gathered}
$$&lt;/p&gt;
&lt;p&gt;每次迭代完成后得到的向量 $X^{(t+1)} = (x_1^{(t+1)}, \ldots, x_d^{(t+1)})$ 就作为链的下一状态。由于每次更新都是从条件分布抽样，Gibbs 采样天然保证目标分布为平稳分布，无需显式计算接受概率。&lt;/p&gt;
&lt;p&gt;Gibbs 采样的优势在于简洁高效，只要各维度的条件分布可直接抽样即可。缺点是适用范围受限，要求目标分布的条件分布形式易于采样，否则算法无法实施。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;重要性采样基本原理&lt;/h1&gt;
&lt;p&gt;在复杂概率模型迅速发展的 1990 年代，贝叶斯推断面临着一个核心困难：随着模型结构变得越来越灵活，后验分布 $P(z|x)$ 几乎不可能再通过解析方式获得。尽管当时已有 MCMC 等基于采样的推断方法，但其计算代价在高维模型或大数据场景下往往难以承受，这促使研究者寻找一种更稳定、更具可扩展性的近似思路。&lt;/p&gt;
&lt;p&gt;在这样的背景下，变分推断开始形成系统化框架。它借鉴了统计物理中的平均场思想（Mean-Field Theory），并在概率图模型中被发展成为一种通过优化来逼近后验分布的可计算方法。与 EM 算法类似，VI 同样通过构建分布族并交替优化不同变量来进行推断；不同之处在于，EM 依赖真实后验，而 VI 则使用可控的近似分布 $Q(z)$ 来替代，从而让复杂模型的推断成为可能。&lt;/p&gt;
&lt;p&gt;在构造 ELBO（Evidence Lower Bound）并进行优化的过程中，我们需要处理如下形式的期望项：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_{Q(z)} \Big[ \log P(x, z) \Big]
$$&lt;/p&gt;
&lt;p&gt;这些期望往往无法解析计算，因为真实后验只能以未归一化密度表示，其归一化常数不可获知。这一问题的本质与 MCMC 中遇到的困难是一致的：我们只能计算未归一化的 $\bar{P}(x)$ ，却无法直接求解其积分，因此许多必要的期望均无法显式写出。&lt;/p&gt;
&lt;p&gt;在此情形下，&lt;strong&gt;重要性采样（Importance Sampling）&lt;/strong&gt; 提供了一种从可采样分布转移到目标分布的基本技术手段。通过从一个易处理的提议分布中采样并施加权重修正，可以构造对原期望的近似估计。该思想既构成了 VI 中随机梯度估计方法的理论基础，也与 MCMC 中的若干技巧共享相同的数学机制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Capproximate-inference%5C%E9%87%8D%E8%A6%81%E6%80%A7%E9%87%87%E6%A0%B71.jpg&quot; alt=&quot;重要性采样图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;在许多推断任务中，我们希望计算某个关于目标分布 $P(x)$ 的期望，例如：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P \Big[ f(x) \Big] = \int f(x) P(x) , dx
$$&lt;/p&gt;
&lt;p&gt;但与 MCMC 所面对的困难相同，目标分布通常只能以未归一化形式出现：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \frac{1}{Z} \bar{P}(x)
$$&lt;/p&gt;
&lt;p&gt;而 $Z$ 在高维空间中不可计算，直接从 $P(x)$ 抽样也往往不可行，因此期望同样无法通过普通的蒙特卡洛方法估计。重要性采样的基本思想是：即使无法直接从目标分布抽样，我们仍可以借助一个易于采样的分布 $Q(x)$ 来间接估计期望。&lt;/p&gt;
&lt;p&gt;为此将期望写成对 $Q(x)$ 的积分形式：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P \Big[ f(x) \Big] = \int f(x) \frac{P(x)}{Q(x)} Q(x) , dx = \mathbb{E}_Q \Big[ f(x) , w(x) \Big] ,
$$&lt;/p&gt;
&lt;p&gt;其中 $\displaystyle w(x) = \frac{P(x)}{Q(x)} = \frac{\bar{P}(x)}{Q(x)} \cdot \frac{1}{Z}$ 是 &lt;strong&gt;重要性权重（importance weight）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于归一化常数 $Z$ 不可得，我们直接使用未归一化权重：&lt;/p&gt;
&lt;p&gt;$$
\bar{w}(x) = \frac{\bar{P}(x)}{Q(x)}
$$&lt;/p&gt;
&lt;p&gt;将其归一化即可得到可操作的形式：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P \Big[ f(x) \Big]
= \frac{\int f(x) \bar{P}(x) , dx}{\int \bar{P}(x) , dx}
= \frac{\int f(x) \frac{\bar{P}(x)}{Q(x)} Q(x) , dx}{\int \frac{\bar{P}(x)}{Q(x)} Q(x) , dx}
= \frac{\mathbb{E}_q \big[ f(x) \bar{w}(x) \big]}{\mathbb{E}_q \big[ \bar{w}(x) \big]}
$$&lt;/p&gt;
&lt;p&gt;在实际操作中，可以通过采样进行估计：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P \Big[ f(x) \Big] = \frac{\mathbb{E}&lt;em&gt;Q \big[ f(x) \bar{w}(x) \big]}{\mathbb{E}&lt;em&gt;Q \big[ \bar{w}(x) \big]} \approx \frac{\sum&lt;/em&gt;{i=1}^N f(x_i) , \bar{w}(x_i)}{\sum&lt;/em&gt;{i=1}^N \bar{w}(x_i)} \quad x_i \sim Q(x)
$$&lt;/p&gt;
&lt;p&gt;这就是重要性采样的核心估计公式。&lt;/p&gt;
&lt;h2&gt;有效样本量（Effective Sample Size）&lt;/h2&gt;
&lt;p&gt;在实际应用中，重要性采样的效率很大程度上取决于 &lt;strong&gt;权重的分布情况&lt;/strong&gt; 。如果提议分布 $Q(x)$ 与目标分布 $P(x)$ 差距较大，少数样本可能占据了几乎全部权重，从而导致估计的方差非常大。为量化这一现象，可以引入 &lt;strong&gt;有效样本量&lt;/strong&gt; 的概念：&lt;/p&gt;
&lt;p&gt;$$
\text{ESS} = \frac{\left(\sum_{i=1}^{N} \bar{w}&lt;em&gt;i\right)^2}{\sum&lt;/em&gt;{i=1}^{N} \bar{w}_i^2}
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{w}_i$ 是归一化前的未归一化权重。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当所有权重接近均匀时，$\text{ESS} \approx N$ ，说明几乎所有样本都有效。&lt;/li&gt;
&lt;li&gt;当权重极度不均时，$\text{ESS} \ll N$ ，说明实际上只有少数样本对估计贡献明显。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 ESS 可以用作 &lt;strong&gt;衡量重要性采样效率的指标&lt;/strong&gt;：ESS 越大，采样越可靠；ESS 越小，则可能需要调整提议分布 $Q(x)$ 或增加样本数量。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;变分推断基本原理&lt;/h1&gt;
&lt;p&gt;在贝叶斯统计中，我们试图求解潜变量的 &lt;strong&gt;后验分布&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
P(z | x) = \frac{P(x, z)}{P(x)}
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 为观测数据，$z$ 为潜变量，$P(x, z)$ 为联合分布。&lt;/p&gt;
&lt;p&gt;而对于大部分实际问题，分母项 $\displaystyle P(x) = \int P(x, z) dz$ 的积分往往缺乏解析形式，无法直接计算。&lt;/p&gt;
&lt;p&gt;传统方法如马尔可夫链蒙特卡洛（MCMC）可以通过采样近似后验，但在数据规模庞大或模型高度复杂时，MCMC 的计算代价通常难以接受。因此，我们希望找到一种既能有效近似后验，又具备较高计算效率的方法。这正是 &lt;strong&gt;变分推断&lt;/strong&gt;（Variational Inference，简称 VI）提出的动机所在：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不直接求解后验，而是在一个可控的分布族中寻找最接近真实后验的分布，通过优化实现推断。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;换句话说，变分推断将一个难以处理的积分问题 &lt;strong&gt;转化为优化问题&lt;/strong&gt; 。通过最大化一个称为 ELBO（Evidence Lower Bound）的目标函数，即可得到后验分布的近似形式。&lt;/p&gt;
&lt;p&gt;从这个角度看，变分推断不仅是一种近似推断方法，更体现了 &lt;strong&gt;概率推断与优化方法结合&lt;/strong&gt; 的思想：面对复杂模型时，接受无法完全精确的事实，通过优化获得最优近似，是一种可行且高效的解决路径。&lt;/p&gt;
&lt;p&gt;以下从 ELBO 的推导开始，逐步介绍变分推断的理论基础与主要算法步骤。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Capproximate-inference%5C%E5%8F%98%E5%88%86%E6%8E%A8%E6%96%AD1.jpg&quot; alt=&quot;变分推断图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;证据下界（Evidence Lower Bound Optimization）&lt;/h2&gt;
&lt;p&gt;变分推断的核心是最大化 ELBO，使得变分分布尽可能接近真实后验。后验分布可写为：&lt;/p&gt;
&lt;p&gt;$$
P(z | x) = \frac{P(x, z)}{P(x)}
$$&lt;/p&gt;
&lt;p&gt;直接最小化 $\displaystyle \text{KL}\Big(q(z)|P(z|x)\Big)$ 不切实际，因为 $P(x)$ 很难计算。通过代入后验的定义，我们从 KL 散度展开：&lt;/p&gt;
&lt;p&gt;$$
\text{KL}\Big(q(z) \parallel P(z|x)\Big) = \int q(z) \log \frac{q(z)}{P(z|x)} , dz
$$&lt;/p&gt;
&lt;p&gt;把后验分布代入公式可得：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\text{KL}\Big(q(z) \parallel P(z|x)\Big)
&amp;amp;= \int q(z) \log \frac{q(z)}{P(x,z)/P(x)} , dz \
&amp;amp;= \int q(z) \log \frac{q(z)}{P(x,z)} , dz + \int q(z) \log P(x) , dz
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;因为 $\log P(x)$ 与 $z$ 无关且 $\displaystyle \int q(z) , dz = 1$ ，所以公式可以化简为：&lt;/p&gt;
&lt;p&gt;$$
\text{KL}\Big(q(z) \parallel P(z|x)\Big) = \int q(z) \log \frac{q(z)}{P(x, z)} , dz + \log P(x)
$$&lt;/p&gt;
&lt;p&gt;于是得到：&lt;/p&gt;
&lt;p&gt;$$
\log P(x) = \underbrace{\int q(z) \log \frac{P(x, z)}{q(z)} dz}_{ELBO} + KL\Big(q(z) \parallel P(z|x)\Big)
$$&lt;/p&gt;
&lt;p&gt;$$
ELBO(q) := \mathbb{E}_{q(z)}\Big[\log P(x, z) - \log q(z)\Big]
$$&lt;/p&gt;
&lt;p&gt;由 KL 散度非负可知：&lt;/p&gt;
&lt;p&gt;$$
\log P(x) \geq \underbrace{\mathbb{E}&lt;em&gt;{q(z)}\Big[\log P(x, z) - \log q(z)\Big]}&lt;/em&gt;{ELBO}
$$&lt;/p&gt;
&lt;p&gt;这说明 &lt;strong&gt;最大化 ELBO 等价于最小化变分分布与真实后验之间的 KL 散度&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;在定义了 ELBO 之后，变分推断的任务可以表述为：在一个参数化的分布族 $q(z;\lambda)$ 内，寻找最能逼近真实后验 $P(z|x)$ 的分布。&lt;/p&gt;
&lt;p&gt;我们的优化目标为：&lt;/p&gt;
&lt;p&gt;$$
ELBO(\lambda) = \mathbb{E}_{q(z;\lambda)}\Big[\log P(x, z) - \log q(z; \lambda)\Big]
$$&lt;/p&gt;
&lt;p&gt;从算法角度，变分推断可以概括为以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择变分簇&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先需要事先指定一个参数化分布族 $q(z;\lambda)$ 作为近似后验。选择合适的变分族对于近似效果至关重要。常用的策略包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;均值场近似&lt;/strong&gt;：假设潜变量之间相互独立，将整体变分分布分解为若干因子 $\displaystyle q(z) = \prod_i q_i(z_i)$ ，这种假设简化了 ELBO 的计算，使优化可以在每个因子上分别进行，但可能忽略潜变量之间的依赖关系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高斯分布近似&lt;/strong&gt;：适用于连续潜变量，特别是在高维空间中，参数化为均值和协方差矩阵。它可以较好地捕捉潜变量的集中趋势，但对多峰后验的拟合能力有限。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;神经网络参数化分布&lt;/strong&gt;：在深度生成模型（如变分自编码器 VAE）中，将变分分布 $q_{\phi}(z|x)$ 通过神经网络进行参数化，使其能够表示更加复杂的后验形状。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;变分族的选择直接决定可表达性与计算开销，是变分推断的重要设计环节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构建 ELBO&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;确定变分族后，将 ELBO 写成关于参数 $\lambda$ 的函数，使其成为可以优化的目标函数：&lt;/p&gt;
&lt;p&gt;$$
ELBO(\lambda) = \mathbb{E}&lt;em&gt;{q(z;\lambda)}\Big[\log P(x, z)\Big] - \mathbb{E}&lt;/em&gt;{q(z;\lambda)}\Big[\log q(z; \lambda)\Big]
$$&lt;/p&gt;
&lt;p&gt;其中第一项鼓励 $q$ 聚集在联合概率大的区域，第二项防止分布过度收缩。&lt;/p&gt;
&lt;p&gt;将 ELBO 转化为可计算、可微的形式后，即可采用数值优化方法求解。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优化 ELBO&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据模型结构与可解析程度的不同，有两类优化方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解析更新&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于小规模模型或结构化模型，可以使用 Coordinate Ascent（坐标上升）方法，依次优化每个变分因子。通过解析公式更新每个 $q_i(z_i)$ ，ELBO 会逐步增加，直至收敛。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;随机梯度优化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于大规模或深度模型，ELBO 通常无法解析计算梯度，需要使用 Monte Carlo 估计：&lt;/p&gt;
&lt;p&gt;$$
\nabla_{\lambda} ELBO(\lambda) \approx \frac{1}{S} \sum_{s=1}^{S} \nabla_{\lambda} \Big[ \log P(x, z^{(s)}) - \log q(z^{(s)}; \lambda) \Big] \quad z^{(s)} \sim q(z; \lambda)
$$&lt;/p&gt;
&lt;p&gt;在此框架下，常使用重参数化技巧（Reparameterization Trick）降低梯度方差，提高优化稳定性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过迭代优化后，最终获得的 $q(z;\lambda^*)$ 即为对真实后验的近似。通过这种方式，变分推断将原本难以计算的积分问题转化为一个标准的优化问题，从而实现高效而可控的概率推断。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;均值场变分推断（Mean-Field Variational Inference）&lt;/h2&gt;
&lt;p&gt;在实际应用中，即便将推断问题转化为优化问题，ELBO 的形式仍可能非常复杂，直接在高维参数空间上进行优化并不现实。为了使问题可处理，常采用一种简化结构假设，即 &lt;strong&gt;均值场&lt;/strong&gt; 假设。其基本思想是将整体潜变量分解为若干独立因子，从而将原本高维耦合的变分分布转化为可分解的形式。&lt;/p&gt;
&lt;h3&gt;均值场假设&lt;/h3&gt;
&lt;p&gt;假设潜变量 $z = (z_1, \ldots, z_m)$ ，均值场方法将变分分布写成乘积形式：&lt;/p&gt;
&lt;p&gt;$$
q(z) = \prod_{i=1}^{m} q_i(z_i)
$$&lt;/p&gt;
&lt;p&gt;该假设消除了潜变量之间的依赖，使得 ELBO 分解为关于各个变分因子的项，从而能够对每个 $q_i$ 分别优化。虽然牺牲了表达能力，但换来了可解性与计算效率。&lt;/p&gt;
&lt;p&gt;在均值场假设下，优化目标为：&lt;/p&gt;
&lt;p&gt;$$
\max_{q_1, \ldots, q_m} ELBO(q_1, \ldots, q_m)
$$&lt;/p&gt;
&lt;p&gt;由于所有因子相互独立，可以采用 &lt;strong&gt;坐标上升&lt;/strong&gt; 逐个更新每个 $q_i$ 。每次更新都能保证 ELBO 不下降，最终收敛到一个局部最优点。&lt;/p&gt;
&lt;h3&gt;坐标上升方法&lt;/h3&gt;
&lt;p&gt;在均值场框架下，一个基本而重要的结果是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;固定其余变分因子 $q_j(z_j)$，最优的 $q_i(z_i)$ 具有以下形式：&lt;/p&gt;
&lt;p&gt;$$
\log q_i^*(z_i) = \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}} \Big[ \log P(x, z) \Big] + \text{const}
$$&lt;/p&gt;
&lt;p&gt;其中 $q_{-i} := \prod_{j \neq i} q_j(z_j)$ 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一结果表明：更新某个变分因子时，只需计算联合分布 $\log P(x,z)$ 在其它变量上的条件期望，随后对结果归一化即可。该公式构成均值场变分推断的基础。&lt;/p&gt;
&lt;p&gt;推导来自对 ELBO 的泛函求导，并结合 $q_i$ 的归一化约束。结论意味着：在固定其他因子的情况下，ELBO 关于 $q_i$ 的极值解自然呈现为指数族形式。&lt;/p&gt;
&lt;p&gt;将上式写为更加直观的指数形式：&lt;/p&gt;
&lt;p&gt;$$
q_i^*(z_i) \propto \exp \Big( \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}} \big[ \log P(x, z) \big] \Big)
$$&lt;/p&gt;
&lt;p&gt;因此，在均值场更新中，对各个因子依次进行如下更新：&lt;/p&gt;
&lt;p&gt;$$
q_i \leftarrow \frac{\exp \Big( \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}} \big[ \log P(x, z) \big] \Big)}{\int \exp \Big( \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}} \big[ \log P(x, z) \big] \Big) dz_i}
$$&lt;/p&gt;
&lt;p&gt;这一循环更新过程保证 ELBO 单调上升，从而逐步逼近最优变分分布。&lt;/p&gt;
&lt;h3&gt;解析化更新公式&lt;/h3&gt;
&lt;p&gt;在均值场框架中，若模型属于 &lt;strong&gt;指数族&lt;/strong&gt; 并且采用了 &lt;strong&gt;共轭先验（Conjugate Prior）&lt;/strong&gt;，坐标上升的更新公式不仅具有一般形式：&lt;/p&gt;
&lt;p&gt;$$
q_i^*(z_i) \propto \exp \Big( \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}} \big[ \log P(x, z) \big] \Big)
$$&lt;/p&gt;
&lt;p&gt;并且能够进一步化为 &lt;strong&gt;完全解析、无需数值积分&lt;/strong&gt; 的更新。正是这种结构，使得均值场变分推断在许多经典概率模型中能够高效实施。具体的解析化条件为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指数族结构&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设联合分布可写成指数族形式：&lt;/p&gt;
&lt;p&gt;$$
P(x, z) = h(x, z) \exp \Big( \eta^\top T(x, z) - A(\eta) \Big)
$$&lt;/p&gt;
&lt;p&gt;其中 $\eta$ 为自然参数，$T(\cdot)$ 为充分统计量。将其代入均值场更新公式后可得：&lt;/p&gt;
&lt;p&gt;$$
\log q_i^*(z_i) = \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}}\Big[\eta^{\rm T} T(x, z)\Big] + \text{const}
$$&lt;/p&gt;
&lt;p&gt;由于指数族在指数上是线性的，上式依旧保持指数族形状，也就是说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;均值场更新不会改变分布的函数族，只会改变自然参数。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此更新某个因子时，其分布类型保持不变，仅需更新自然参数即可。这让整个优化步骤变得非常规整。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;共轭先验&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;若模型在指数族框架下采用共轭先验，则自然参数的期望在数学上具有封闭表达式。对于常见的指数族分布，模型结构保证：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;后验的自然参数等于先验参数加上数据充分统计量的期望。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，上述更新公式可进一步写成：&lt;/p&gt;
&lt;p&gt;$$
\eta_i^{\text{new}} = \eta_i^{\text{prior}} + \mathbb{E}&lt;em&gt;{q&lt;/em&gt;{-i}}\Big[T_i(x, z)\Big]
$$&lt;/p&gt;
&lt;p&gt;其中 $T_i(x,z)$ 为涉及 $z_i$ 的部分充分统计量。&lt;/p&gt;
&lt;p&gt;这一形式具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无需数值积分/采样&lt;/strong&gt;：所有期望都是指数族之间的期望，天然就有解析解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新步骤可解析&lt;/strong&gt;：每次更新均为代数运算，不需要采样或数值优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ELBO 自动单调上升&lt;/strong&gt;：解析更新保证每步都是精确坐标上升，收敛性好、行为稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;随机变分推断（Stochastic Variational Inference）&lt;/h2&gt;
&lt;p&gt;在前面对变分推断的讨论中，我们默认数据量是 “可控” 的：ELBO 的梯度、充分统计量的期望等都可以对全数据一次性计算。但随着模型应用到真正的大规模数据（数百万、甚至上亿条样本），这种 “全数据计算一次” 的方式就已经完全不现实了。&lt;strong&gt;随机变分推断&lt;/strong&gt;（Stochastic Variational Inference，简称 SVI）就是在这种背景下诞生的。&lt;/p&gt;
&lt;p&gt;SVI 的核心理念很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;把变分推断从 “全数据批处理” 改成 “随机小批量更新” 。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，不再每次都使用全部样本来计算 ELBO 的梯度或期望，而是随机抽取一个 mini-batch，根据这一小部分数据构造对全局梯度的无偏估计，从而进行一次变分参数的更新。这样的改动，使得变分推断能够像随机梯度下降一样，在海量数据下也能持续推进。&lt;/p&gt;
&lt;h3&gt;局部变量与全局变量&lt;/h3&gt;
&lt;p&gt;在很多分层贝叶斯模型中，潜变量天然分成两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;局部变量（local variables）&lt;/strong&gt;：与单个数据点相关，比如混合模型中的簇分配、LDA 中某篇文档的主题比例。每个样本一套，它们数量随数据量线性增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局变量（global variables）&lt;/strong&gt;：共享于所有数据，比如主题词分布、混合模型的各成分参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统变分推断要同时优化所有局部和全局变量，但当数据量巨大时局部变量数量爆炸，无法训练。而 SVI 的关键是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;对每个 mini-batch，只更新对应的局部变量，并用它们构成对全局变量梯度的无偏估计，然后更新全局变量。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;局部变量不会全部常驻内存，因此模型能在大数据上运行。&lt;/p&gt;
&lt;h3&gt;自然梯度更新&lt;/h3&gt;
&lt;p&gt;SVI 中的一项重要技术是 &lt;strong&gt;自然梯度（Natural Gradient）&lt;/strong&gt;。原因并非 “自然梯度更高级” ，而是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在指数族 + 共轭先验的框架下，自然梯度给出的更新形式和我们在均值场下的 “自然参数更新” 完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此 SVI 的全局参数更新非常简单，类似：&lt;/p&gt;
&lt;p&gt;$$
\lambda^{(t+1)} = (1 - \rho_t) \lambda^{(t)} + \rho_t , \hat{\lambda}_{\text{new}}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\lambda$ 是全局变分参数（自然参数）&lt;/li&gt;
&lt;li&gt;$\hat{\lambda}_{\text{new}}$ 是由当前 mini-batch 计算出的后验自然参数&lt;/li&gt;
&lt;li&gt;$\rho_t$ 是学习率，一般取 Robbins–Monro 序列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种更新方式保证了收敛性，同时保留了变分推断的结构优势。&lt;/p&gt;
&lt;h3&gt;流程介绍&lt;/h3&gt;
&lt;p&gt;随机变分推断的核心理念在于巧妙地平衡了 &lt;strong&gt;“大规模数据处理的计算限制”&lt;/strong&gt; 与 &lt;strong&gt;“有效利用全部数据的推断需求”&lt;/strong&gt; 这一对矛盾。SVI避开了对全数据的直接推断，转而利用 &lt;strong&gt;随机抽样&lt;/strong&gt; ，在每一次迭代中从完整数据集中获取一个 &lt;strong&gt;mini-batch&lt;/strong&gt; 。这一机制相当于在庞大的观测空间中捕捉一个 &lt;strong&gt;局部快照&lt;/strong&gt; ，借助局部的精炼信息来驱动模型的全局优化进程。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高效的局部更新&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;获得 mini-batch 后，第一步是更新与这批样本相关的&lt;strong&gt;局部变分分布&lt;/strong&gt; $q(z_{\text{local}})$。&lt;/p&gt;
&lt;p&gt;关键在于，由于局部变量通常具备 &lt;strong&gt;共轭结构&lt;/strong&gt; ，其更新步骤继承了经典 VI 的 &lt;strong&gt;解析性与简洁性&lt;/strong&gt; ————  仅需代入充分统计量并直接更新自然参数。这意味着，我们 &lt;strong&gt;无需&lt;/strong&gt; 进行任何数值优化或采样，单次更新的数学形式并未因数据量的暴增而复杂化，反而保持了极高效率。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局影响的无偏构造&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接下来，SVI 面临的关键挑战是如何将这一小批 &lt;strong&gt;局部信息&lt;/strong&gt; 有效地转化为对 &lt;strong&gt;整个数据集&lt;/strong&gt; 的全局影响。&lt;/p&gt;
&lt;p&gt;解决方案是通过构造 &lt;strong&gt;全局充分统计量的无偏估计&lt;/strong&gt; 来实现：它将 mini-batch 中计算得到的统计量，按照总样本量 $N$ 与批次大小 $B$ 的比例 ( $N/B$ ) 进行放大。这一放大操作并非臆测，而是基于严格的 &lt;strong&gt;无偏性保证&lt;/strong&gt; ———— 理论上，反复抽样的期望值恰好等于全数据的真实统计量。因此，尽管单次更新只利用了数据的片段，但其 &lt;strong&gt;更新方向&lt;/strong&gt; 始终精确地指向逼近全数据真实目标的正确轨道。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;稳健的全局推进&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;凭借这些无偏统计量，我们便能着手更新 &lt;strong&gt;全局变分参数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;为了确保更新的 &lt;strong&gt;稳定性&lt;/strong&gt; 和 &lt;strong&gt;收敛效率&lt;/strong&gt; ，并使其更好地契合指数族分布的几何特性，SVI 通常采用 &lt;strong&gt;自然梯度（Natural Gradient）&lt;/strong&gt; ，而非传统的欧氏梯度。自然梯度具有一项重要特性：它能够 &lt;strong&gt;自动匹配&lt;/strong&gt; 变分分布流形的内在曲率，从而使得更新步幅既 &lt;strong&gt;稳健&lt;/strong&gt; 又 &lt;strong&gt;高效&lt;/strong&gt; ———— 既避免了因学习率过大而引起的优化震荡，也杜绝了步幅过小导致的收敛停滞。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;期望传播基本原理&lt;/h1&gt;
&lt;p&gt;在变分推断（VI）、马尔可夫链蒙特卡洛（MCMC）和重要性采样（IS）等全局近似方法中，推断要么依赖一个整体的可导优化目标，要么直接依赖复杂的采样机制来重构后验。这些方法皆是 &lt;strong&gt;从全局视角出发&lt;/strong&gt; 处理后验分布。&lt;/p&gt;
&lt;p&gt;然而在许多贝叶斯模型中，后验分布往往具有清晰的 &lt;strong&gt;可分解的因子结构&lt;/strong&gt;：由先验和大量局部似然项以乘积形式组成。EP 的核心思想是 &lt;strong&gt;化整为零&lt;/strong&gt; ，它不从整体入手，而是让每个因子通过 &lt;strong&gt;局部信息传递&lt;/strong&gt; 和 &lt;strong&gt;反复协商&lt;/strong&gt; ，逐步校准全局近似分布，直至收敛稳定。&lt;/p&gt;
&lt;p&gt;EP 避免直接处理全局 KL 散度，而是在 “因子—全局” 的往复过程中，利用反向 KL 投影逐步调整近似分布，使其尽可能保留真实后验的局部矩信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Capproximate-inference%5C%E6%9C%9F%E6%9C%9B%E4%BC%A0%E6%92%AD1.jpg&quot; alt=&quot;期望传播图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;因子结构（Factorization Structure）&lt;/h2&gt;
&lt;p&gt;EP 的起点是将目标后验写成若干局部因子的乘积：&lt;/p&gt;
&lt;p&gt;$$
P(\theta | x) \propto P(\theta) \prod_{i=1}^{n} P(x_i | \theta) = \prod_{i=0}^{n} t_i(\theta)
$$&lt;/p&gt;
&lt;p&gt;其中 $t_0(\theta) = P(\theta)$ 为先验因子，其余 $t_i(\theta)$ 对应似然项。&lt;/p&gt;
&lt;p&gt;EP 并不要求因子间的独立性，而是在算法设计上引入一组 &lt;strong&gt;可处理的&lt;/strong&gt; 近似因子 $\bar{t}_i(\theta) \in \mathcal{Q}$ ，使得所有近似因子的乘积仍属于某个选定的指数族 $\mathcal{Q}$ ：&lt;/p&gt;
&lt;p&gt;$$
q(\theta) \propto \prod_{i=0}^{n} \bar{t}_i(\theta) \quad q(\theta) \in \mathcal{Q}
$$&lt;/p&gt;
&lt;p&gt;若采用指数族形式 $q(\theta) = \exp \Big( \eta^{\rm T} u(\theta) - A(\eta) \Big)$ ，则每个 site 近似因子 $\bar{t}_i(\theta)$ 也可以写成指数族片段：&lt;/p&gt;
&lt;p&gt;$$
\bar{t}_i(\theta) = \exp \Big( \lambda_i^{\rm T} u(\theta) \Big)
$$&lt;/p&gt;
&lt;p&gt;因此 EP 的因子结构不仅是简单的乘积分解，更是通过 &lt;strong&gt;自然参数的线性叠加&lt;/strong&gt; $\eta = \sum \lambda_i$ 来维持一个始终处于指数族 $\mathcal{Q}$ 内的全局近似后验 $q(\theta)$ 。&lt;/p&gt;
&lt;h2&gt;局部分布（Local Distribution）&lt;/h2&gt;
&lt;p&gt;在更新第 $i$ 个因子时，EP 需要计算两个关键的中间分布：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cavity Distribution（空腔分布）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
q_{-i}(\theta) \propto \frac{q(\theta)}{\bar{t}&lt;em&gt;i(\theta)} = \prod&lt;/em&gt;{j \neq i} \bar{t}_j(\theta)
$$&lt;/p&gt;
&lt;p&gt;它衡量的是 &lt;strong&gt;排除当前近似因子 $\bar{t}_i$ 影响&lt;/strong&gt; 后的全局近似分布。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tilted Distribution（倾斜分布）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\bar{P}&lt;em&gt;i(\theta) \propto q&lt;/em&gt;{-i}(\theta) , t_i(\theta)
$$&lt;/p&gt;
&lt;p&gt;它是将 Cavity 分布与 &lt;strong&gt;真实的局部因子 $t_i(\theta)$&lt;/strong&gt; 相乘得到的。&lt;strong&gt;倾斜分布承载了待投影的真实局部信息&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于 $\bar{P}&lt;em&gt;i(\theta)$ 通常不属于我们选定的指数族 $\mathcal{Q}$ ，我们无法直接用其作为全局表达，而只能从中抽取 &lt;strong&gt;充分统计量&lt;/strong&gt;（如期望 $m_i$、协方差 $S_i$ 等）作为 EP 迭代的 &lt;strong&gt;关键输入&lt;/strong&gt;：
$$
\mathbb{E}&lt;/em&gt;{\bar{P}_i}[u(\theta)] = \int u(\theta) \bar{P}_i(\theta) d\theta
$$&lt;/p&gt;
&lt;h2&gt;投影机制（Moment Projection）&lt;/h2&gt;
&lt;p&gt;EP 的更新本质上是通过 &lt;strong&gt;矩匹配投影&lt;/strong&gt; ，将 Tilted Distribution 中蕴含的真实局部信息 “压缩” 到指数族 $\mathcal{Q}$ 中，以校准全局近似。这一投影定义为最小化 &lt;strong&gt;反向 KL 散度&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
q^{\text{new}}(\theta) = \arg \min_{q \in \mathcal{Q}} \text{KL}\Big(\bar{P}_i(\theta) \parallel q(\theta)\Big)
$$&lt;/p&gt;
&lt;p&gt;这也是 EP 与 VI 最根本的区别：EP 采用反向 KL 散度。与 VI 的前向 KL 倾向于收缩分布不同，反向 KL 倾向于 &lt;strong&gt;覆盖真实分布的支撑域&lt;/strong&gt; ，从而避免了近似分布过度收缩（零塌陷）的问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于正向 KL 和反向 KL 的区别可以看下面这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114558102410096&amp;amp;bvid=BV1r6jHzpE1J&amp;amp;cid=30166354742&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;在指数族框架下，最小化反向 KL 等价于让新的近似分布 $q^{\text{new}}$ 与 $\bar{P}_i$ &lt;strong&gt;匹配同一组充分统计量（矩匹配）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}&lt;em&gt;{q^{\text{new}}}\Big[u(\theta)\Big] = \mathbb{E}&lt;/em&gt;{\bar{P}_i}\Big[u(\theta)\Big]
$$&lt;/p&gt;
&lt;p&gt;完成投影（矩匹配）后，新的 site 因子 $\bar{t}_i^{\text{new}}$ 通过下面的式子 &lt;strong&gt;唯一确定&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\bar{t}&lt;em&gt;i^{\text{new}}(\theta) \propto \frac{q^{\text{new}}(\theta)}{q&lt;/em&gt;{-i}(\theta)}
$$&lt;/p&gt;
&lt;p&gt;这个更新将局部的真实信息传回到全局近似中，同时保持了指数族结构。&lt;/p&gt;
&lt;p&gt;在自然参数空间中，更新表现为参数的相减：&lt;/p&gt;
&lt;p&gt;$$
\lambda_i^{\text{new}} = \eta^{\text{new}} - \eta_{-i}
$$&lt;/p&gt;
&lt;p&gt;其中 $\eta^{\text{new}}$ 和 $\eta_{-i}$ 分别是 $q^{\text{new}}$ 和 $q_{-i}$ 的自然参数。&lt;/p&gt;
&lt;p&gt;为提高稳定性，实际实现常采用 &lt;strong&gt;阻尼（Damping）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\lambda_i \leftarrow (1 - \rho) \lambda_i + \rho \lambda_i^{\text{new}} \quad 0 &amp;lt; \rho \leq 1
$$&lt;/p&gt;
&lt;h2&gt;迭代动力学（Iterative Dynamics）&lt;/h2&gt;
&lt;p&gt;若把所有 site 参数组合成全局自然参数向量 $\eta$，EP 更新对应求解一个 &lt;strong&gt;定点方程&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\eta = F(\eta)
$$&lt;/p&gt;
&lt;p&gt;其中 $F$ 是由 Cavity-Tilted-Projection 三步构成的非线性映射。&lt;/p&gt;
&lt;p&gt;由于该映射 $F$ 通常 &lt;strong&gt;不对应某个显式的优化目标&lt;/strong&gt; ，EP 缺乏 VI 的 &lt;strong&gt;变分下界保证&lt;/strong&gt; ，也不具备 MCMC 的 &lt;strong&gt;渐近一致性&lt;/strong&gt; 。其收敛性分析依赖于局部线性化，通过调节雅可比矩阵 $J$ 的谱半径来确保收敛。&lt;/p&gt;
&lt;p&gt;尽管缺乏全局理论保证，EP 在许多模型中表现出良好的数值稳定性。其优势在于反向 KL 散度 $\text{KL}(\tilde{P}_i \parallel q)$ 确保了近似后验 $q$ 能够 &lt;strong&gt;尽可能保留&lt;/strong&gt; 真实分布的支撑域，并在多模态（Multimodal）或强相关结构下，维持 &lt;strong&gt;更大的方差、更宽的尾部和更忠实的协方差结构&lt;/strong&gt; 。这是 EP 在高斯过程、鲁棒回归等非共轭模型中，表现常优于标准 VI 的主要原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题思考&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么马尔可夫链可以使得采样变得容易？如何设计 MCMC 算法以确保链收敛到目标分布？&lt;/p&gt;
&lt;h3&gt;转移算子（Markov Operator）&lt;/h3&gt;
&lt;p&gt;直接从后验分布中采样困难的根源在于我们通常只知道未归一化形式：&lt;/p&gt;
&lt;p&gt;$$
P(\theta| x) \propto P(\theta)P(x|\theta)
$$&lt;/p&gt;
&lt;p&gt;而归一化常数 $\displaystyle Z = \int P(\theta)P(x| \theta)d\theta$ 往往无法显式计算。构造独立样本等价于对整个高维分布的全局积分或累积分布求逆，这是高维问题几乎无法直接完成的。&lt;/p&gt;
&lt;p&gt;MCMC 引入的关键思想是：&lt;strong&gt;不再尝试直接构造独立样本，而是构造一个能把任意初始分布推向目标分布的转移算子&lt;/strong&gt; 。这个转移算子只需要依赖 &lt;strong&gt;局部密度比&lt;/strong&gt; $\displaystyle \frac{P(\theta&apos;)}{P(\theta)}$ ，而不需要知道全局归一化常数。这使得采样从一个全局难题转化为一个局部可控的更新机制。&lt;/p&gt;
&lt;p&gt;转移算子在连续空间中是积分算子：&lt;/p&gt;
&lt;p&gt;$$
(Tf)(\theta&apos;) = \int f(\theta) K(\theta&apos;|\theta) d\theta
$$&lt;/p&gt;
&lt;p&gt;在离散状态下就是行随机矩阵。任意初始分布 $\mu_0$ 在迭代 $t$ 后的分布为：&lt;/p&gt;
&lt;p&gt;$$
\mu_t = \mu_0 K^t
$$&lt;/p&gt;
&lt;h3&gt;平稳分布（Stationary Distribution）&lt;/h3&gt;
&lt;p&gt;将转移核 $K(\theta&apos;|\theta)$ 看成一个积分算子（或离散情况下的随机矩阵 $K$ ）。平稳分布的定义是：&lt;/p&gt;
&lt;p&gt;$$
\pi = \pi K
$$&lt;/p&gt;
&lt;p&gt;即 $\pi$ 是 $K$ 的 &lt;strong&gt;左特征向量&lt;/strong&gt; ，对应特征值 1。这意味着，如果初始分布已经是平稳分布，应用转移算子不会改变它。&lt;/p&gt;
&lt;p&gt;行随机矩阵的性质保证 1 必然是右特征值：&lt;/p&gt;
&lt;p&gt;$$
\sum_{\theta&apos;} K(\theta&apos;|\theta) = 1 \quad \Rightarrow \quad K^{\rm T} \mathbf{1} = \mathbf{1}
$$&lt;/p&gt;
&lt;p&gt;因此，构造 MCMC 的目标就是让目标后验 $\pi = P(\theta | x)$ 成为转移矩阵的平稳分布，换句话说，它是左特征向量对应特征值 1。&lt;/p&gt;
&lt;h3&gt;细致平衡（Detailed Balance）&lt;/h3&gt;
&lt;p&gt;细致平衡条件为：&lt;/p&gt;
&lt;p&gt;$$
\pi(\theta)K(\theta&apos;|\theta)=\pi(\theta&apos;)K(\theta|\theta&apos;)
$$&lt;/p&gt;
&lt;p&gt;它的数学本质是让转移算子在加权内积下成为 &lt;strong&gt;自伴算子（Self-Adjoint Operator）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\langle f,g\rangle_\pi=\sum_\theta f(\theta)g(\theta)\pi(\theta)
$$&lt;/p&gt;
&lt;p&gt;自伴算子保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;谱实数化且可对角化&lt;/li&gt;
&lt;li&gt;最大特征值为 1，其余特征值满足 $|\lambda_k|&amp;lt;1$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，任意初始分布在迭代高次幂 $K^t$ 后：&lt;/p&gt;
&lt;p&gt;$$
\mu_0 K^t
= \pi + \sum_{k\ge 2} c_k \lambda_k^t v_k \quad\longrightarrow\quad \pi
$$&lt;/p&gt;
&lt;p&gt;高阶分量衰减至零，确保收敛到唯一平稳分布。&lt;/p&gt;
&lt;h3&gt;局部密度比（Local Density Ratio）&lt;/h3&gt;
&lt;p&gt;MCMC 能绕开归一化常数的根本原因是 &lt;strong&gt;转移核只依赖目标分布的比值&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\frac{\pi(\theta&apos;)}{\pi(\theta)}=\frac{P(\theta&apos;)P(x\mid\theta&apos;)}{P(\theta)P(x\mid\theta)}
$$&lt;/p&gt;
&lt;p&gt;归一化常数 $Z$ 在比值中相互抵消。也就是说，算法根本无需计算全局积分，只需要 &lt;strong&gt;局部相对概率&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;从更深层看，马尔可夫链不需要显式构造整个分布，而是通过谱结构把任意初始分布 “推” 向平稳分布。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自伴性保证特征谱可控&lt;/li&gt;
&lt;li&gt;不可约性保证平稳分布唯一（左特征向量空间一维）&lt;/li&gt;
&lt;li&gt;非周期性保证所有非主特征值模小于 1，从而消除循环效应&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部密度比是构造这种自伴、不可约、非周期转移算子的核心信息。这就是 MCMC 之所以可行的数学根本：&lt;strong&gt;全局采样难题 → 局部转移构造 → 谱收敛&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重要性采样中的有效样本量为什么是 “和平方” 除以 “平方和” 的形式？（有效样本量公式是如何设计的？）&lt;/p&gt;
&lt;h3&gt;有效样本量（Effective Sample Size）&lt;/h3&gt;
&lt;p&gt;在重要性采样中，我们从一个 &lt;strong&gt;proposal 分布&lt;/strong&gt; $q(\theta)$ 采样，并利用权重来估计目标分布 $P(\theta)$ 下的期望 $\mathbb{E}_P\Big[f(\theta)\Big]$ ：&lt;/p&gt;
&lt;p&gt;$$
\hat{\mu} = \frac{\sum_{i=1}^{N} w_i f(\theta_i)}{\sum_{i=1}^{N} w_i}
$$&lt;/p&gt;
&lt;p&gt;虽然我们有 $N$ 个样本，但由于权重不均匀，少数大权重样本可能主导估计，使得方差远大于均匀 i.i.d. 样本的方差。有效样本量 $N_{\text{eff}}$ 的目的是衡量这些带权样本在方差意义上等效于多少个独立同分布样本。&lt;/p&gt;
&lt;h3&gt;公式推导&lt;/h3&gt;
&lt;p&gt;考虑归一化权重下的重要性采样估计，其方差近似为：&lt;/p&gt;
&lt;p&gt;$$
\text{Var}[\hat{\mu}] \approx \frac{\sigma^2 \sum_{i=1}^{N} w_i^2}{\left(\sum_{i=1}^{N} w_i\right)^2}
$$&lt;/p&gt;
&lt;p&gt;将其写成等效样本量形式，即定义：&lt;/p&gt;
&lt;p&gt;$$
N_{\text{eff}} = \frac{\left(\sum_{i=1}^{N} w_i\right)^2}{\sum_{i=1}^{N} w_i^2}
$$&lt;/p&gt;
&lt;p&gt;这个公式的数学直觉是基于 &lt;strong&gt;方差等效原则&lt;/strong&gt;：它量化了权重分布对估计方差的放大作用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当权重均匀时 $w_i = w$ ：&lt;/p&gt;
&lt;p&gt;$$
N_{\text{eff}} = \frac{(Nw)^2}{Nw^2} = N
$$&lt;/p&gt;
&lt;p&gt;说明所有样本同等有效。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当权重高度集中时，大部分权重接近零，少数权重主导总和：&lt;/p&gt;
&lt;p&gt;$$
N_{\text{eff}} \ll N
$$&lt;/p&gt;
&lt;p&gt;反映实际贡献的有效样本数量急剧下降。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;核心原理&lt;/h3&gt;
&lt;p&gt;ESS 衡量的是 &lt;strong&gt;权重分布的均匀性&lt;/strong&gt; 和样本信息量。公式中 “平方和 / 和平方” 的结构正是对归一化权重方差的倒数，直接反映了样本权重分散程度。权重越均匀，ESS 越接近总样本数；权重越集中，ESS 越小。这也解释了为什么在重要性采样中，选择与目标分布接近的 proposal 分布是保证高效采样的关键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么重要性采样引入一个提议分布后就能正常采样？其中的原理与 MCMC 方法是否相同？&lt;/p&gt;
&lt;p&gt;我们想从目标分布 $P(\theta)$ 中计算期望：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P\Big[f(\theta)\Big] = \int f(\theta) P(\theta) , d\theta
$$&lt;/p&gt;
&lt;p&gt;但直接从 $P(\theta)$ 抽样可能困难或不可行。于是引入一个 &lt;strong&gt;易于采样的提议分布&lt;/strong&gt; $q(\theta)$ ，并对样本进行 &lt;strong&gt;权重校正&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
w(\theta) = \frac{p(\theta)}{q(\theta)} \quad \theta \sim q(\theta)
$$&lt;/p&gt;
&lt;p&gt;这样原本在 $P$ 下的积分可以转写为在 $q$ 下的积分：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}_P\Big[f(\theta)\Big] = \int f(\theta) \frac{P(\theta)}{q(\theta)} q(\theta) , d\theta = \mathbb{E}_q\Big[w(\theta)f(\theta)\Big]
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：通过 &lt;strong&gt;权重修正&lt;/strong&gt; ，我们不必直接采样难以处理的 $P(\theta)$ ，而是从一个更简单的 $q(\theta)$ 采样，并用权重把分布 “映射回” 目标分布。&lt;/p&gt;
&lt;p&gt;IS 与 MCMC 的核心区别在于 &lt;strong&gt;采样方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重要性采样仍然是 &lt;strong&gt;独立样本&lt;/strong&gt; ，但需要权重来校正分布&lt;/li&gt;
&lt;li&gt;MCMC 是通过 &lt;strong&gt;依赖样本的马尔可夫链&lt;/strong&gt; 来逼近平稳分布，不需要显式权重，但样本相关，需要长链收敛&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心相似之处在于两者都利用了  &lt;strong&gt;局部或比值信息&lt;/strong&gt; 来避免直接依赖目标分布的归一化常数。这也是它们在高维概率计算中能够可行的根本原因。&lt;/p&gt;
&lt;p&gt;简单说：&lt;strong&gt;重要性采样通过 “全局重加权” 实现目标分布采样，MCMC 通过 “局部迭代更新” 逼近平稳分布采样&lt;/strong&gt; ，本质都是规避直接处理目标分布的全局归一化问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献&lt;/h1&gt;
&lt;h2&gt;马尔科夫蒙特卡洛&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Cnoized/p/18913687&quot;&gt;机器学习-11-马尔科夫链蒙特卡洛MCMC&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/392917306&quot;&gt;马尔可夫蒙特卡罗 MCMC 原理及经典实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://weirping.github.io/blog/Stationary-Distribution-Markov-chain.html&quot;&gt;马尔科夫链及其平稳分布&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;重要性采样&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://allenwind.github.io/blog/10466/&quot;&gt;采样（三）：重要性采样与接受拒绝采样&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/695130713&quot;&gt;重要性采样(Importance Sampling)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://baileyswu.github.io/2019/03/importance-sampling/&quot;&gt;【Ugly Garden】重要性采样&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/jiangweiwang/p/18836935&quot;&gt;【强化学习策略】重要性采样&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;变分推断&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/682453554&quot;&gt;一文搞懂变分推断（Variational inference）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/tshaaa/p/18651129&quot;&gt;VI、SGVI/SGVB、VAE串讲&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1893801387277648020&quot;&gt;变分推断详细推导&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Cnoized/p/18910930&quot;&gt;机器学习-10-变分推断VI&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.yokumi.cn/2025/07/02/%E3%80%90PRML%E3%80%91%E5%A6%82%E4%BD%95%E7%AE%80%E5%8D%95%E6%98%93%E6%87%82%E5%9C%B0%E7%90%86%E8%A7%A3%E5%8F%98%E5%88%86%E6%8E%A8%E6%96%AD%EF%BC%88Variational%20Inference%EF%BC%89/&quot;&gt;【PRML】如何简单易懂地理解变分推断&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1890718815135974325&quot;&gt;变分推断与 ELBO&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1938261633504895739&quot;&gt;变分推断基础教程&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_44009891/article/details/106859748&quot;&gt;贝叶斯统计与变分推断&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;期望传播&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://just.ustc.edu.cn/article/pdf/preview/1643455321968-20539875.pdf&quot;&gt;基于期望传播的活跃用户检测和信道估计&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://scis.scichina.com/cn/2019/N112018-00160.pdf&quot;&gt;基于期望传播的低复杂度高性能 EP-SU 大规模 MIMO 检测&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基础算法】第四节：概率图算法</title><link>https://xingguang641.com/posts/probabilistic-graphical-model/probabilistic-graphical-model/</link><guid isPermaLink="true">https://xingguang641.com/posts/probabilistic-graphical-model/probabilistic-graphical-model/</guid><description>介绍机器学习常见的算法</description><pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;概率图算法背景介绍&lt;/h1&gt;
&lt;p&gt;概率图模型是一类将 &lt;strong&gt;概率分布&lt;/strong&gt; 与 &lt;strong&gt;图结构&lt;/strong&gt; 结合起来的建模方法。它的核心思想是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;高维联合分布之所以难以处理，是因为变量之间的依赖关系过于复杂。而图结构能够清晰表达这些依赖，使得分布分解成为可能，从而让推断和学习变得可行。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;换句话说，概率图模型是一种把抽象的概率关系 “实体化” 为图的技术。通过这种表达方式，原本隐藏在联合分布中的依赖结构被显式地描绘出来，使我们能够直观看到哪些变量直接相关，哪些变量之间的影响是通过其他节点传递的。图的存在让整个模型的结构变得透明，而概率分布的计算也随之获得结构上的支撑。&lt;/p&gt;
&lt;p&gt;在这种框架下，复杂的高维分布不再被视为一个不可分解的整体，而是被拆解成许多局部的小片段，每个片段对应着图中的一个节点或一组节点之间的相互作用。概率图模型的威力正来自这种分解能力：通过将大问题拆成一系列局部问题，我们不仅能够高效计算边缘概率、条件概率，也能够利用图结构进行消息传递、局部归一化、采样等操作，从而实现原本难以实施的推断与学习。&lt;/p&gt;
&lt;p&gt;因此，概率图模型既是一种结构化的概率建模语言，也是一种计算框架。它将概率论的表达能力与图结构的计算特性结合起来，使得我们能够以一种更系统、更可控的方式理解高维随机系统。无论是贝叶斯网络、马尔可夫随机场，还是更复杂的条件随机场与能量模型，其基本思想都可以追溯到概率图模型这一统一的结构化表达体系。&lt;/p&gt;
&lt;h2&gt;因子分解（Factorization）&lt;/h2&gt;
&lt;p&gt;概率图模型的核心思想，是利用图结构来表达变量之间的条件独立关系，并据此对高维联合分布进行分解。最典型的两种结构是贝叶斯网络与马尔可夫随机场，它们分别对应有向图与无向图的两类因子化方式。&lt;/p&gt;
&lt;p&gt;在 &lt;strong&gt;有向图（Bayesian Network）&lt;/strong&gt; 中，边表示因果或条件生成关系，每个节点只依赖于其父节点，因此联合分布天然地分解为若干条件分布的乘积：&lt;/p&gt;
&lt;p&gt;$$
P(x_1, \ldots, x_n) = \prod_i P(x_i \mid pa(x_i))
$$&lt;/p&gt;
&lt;p&gt;而在 &lt;strong&gt;无向图（Markov Random Field）&lt;/strong&gt; 中，边不再代表方向性的因果关系，而是表达节点之间的相互作用。联合分布以团（Clique）为单位拆解为势函数的乘积：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \frac{1}{Z} \prod_{c \in \mathcal{C}} \psi_c(x_c)
$$&lt;/p&gt;
&lt;p&gt;其中的 $\psi_c$ 描述团内部变量的相容程度或能量贡献。&lt;/p&gt;
&lt;p&gt;为了更清晰地展现这种分解结构，人们通常引入 &lt;strong&gt;因子图（Factor Graph）&lt;/strong&gt; 作为统一表示，把联合分布直接写成一系列因子的乘积：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \frac{1}{Z} \prod_i f_i(x_i)
$$&lt;/p&gt;
&lt;p&gt;因子图的优势在于，它把复杂分布拆解成了若干局部因子，每个因子只关注它所连接的少量变量。这种分解让许多原本难以进行的推断变得可操作，例如在消息传递算法中，信息就可以在因子与变量之间局部地传播，而无需处理整个高维空间的全局结构。&lt;/p&gt;
&lt;p&gt;通过这种因子化，高维联合分布从一个不可直接处理的整体，变成了许多简单的局部片段的乘积，使推断与学习都能够在结构化的图上高效地进行。&lt;/p&gt;
&lt;h2&gt;消息传递（Message Passing）&lt;/h2&gt;
&lt;p&gt;在概率图模型里，真正支撑所有推断算法的思想可以概括成一句话：&lt;strong&gt;全局的推断行为，其实可以通过图结构中的局部消息来完成&lt;/strong&gt; 。不同节点之间并不需要共享整个概率分布的信息，它们只需要专注于各自的邻域，通过不断交换关于变量状态的 “局部信念” ，一个复杂的整体分布就能在图中逐渐成形。&lt;/p&gt;
&lt;p&gt;Belief Propagation 就是这个思想最典型的例子。你可以把它想成每个节点都在维护一份关于自己的 “信念”（Belief），然后把这份信念压缩成一条对他人有用的 “消息”（Message）沿着边发出去。一个节点收到来自邻居们的消息后，会用这些信息更新自己的更精确的信念，再把新的消息继续传出去。如此反复更新，等图上所有消息传递得足够充分时，节点对自身变量的估计就会逐渐趋于稳定。&lt;/p&gt;
&lt;p&gt;如果图是树结构，这种消息传递恰好对应精确的贝叶斯推断；而如果图上存在环，消息可能在环中来回传播很多次，但只要最终收敛，人们往往会发现这个收敛值仍然很接近真实结果，因此被称为 Loopy BP。它本质仍然遵循同一套局部更新逻辑，只是消息的迭代次数更多。&lt;/p&gt;
&lt;p&gt;BP 的统一形式非常简单，几乎可以视为 “局部性原则” 的数学写法：&lt;/p&gt;
&lt;p&gt;$$
m_{i \rightarrow j}(x_j) = \sum_{x_i} f_i(x_i) \prod_{k \in N(i) \setminus j} m_{k \rightarrow i}(x_i)
$$&lt;/p&gt;
&lt;p&gt;这个公式的含义其实非常直观：每个节点在给邻居发送消息时，只考虑自己本地的因子，以及来自除该邻居以外的所有其他邻居的消息。它不需要知道整个图长什么样，也不需要处理图中远处的变量关系。换句话说，&lt;strong&gt;全局结构所蕴含的依赖关系，被图结构拆解成了一系列纯粹的局部计算&lt;/strong&gt; ，推断正是靠这些局部计算在图中不断传播、逐步汇聚起来的。&lt;/p&gt;
&lt;h2&gt;核心价值（Core Value）&lt;/h2&gt;
&lt;p&gt;概率图模型真正的价值不在于它提出了新的概率论公式，而在于它为“如何在高维空间中管理复杂依赖”提供了一种系统化、可扩展、而且具有明确结构含义的解决方案。高维随机变量之间的关系往往极其复杂，如果直接从联合分布入手，会面临维度灾难、推断无法计算、表达不可控等问题。PGM 选择从图结构出发，把概率分布拆解成由节点和边组成的局部依赖，再在这个结构上执行各种推断算法。&lt;/p&gt;
&lt;p&gt;具体来说，它的核心价值可以从下面几个方面理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结构化表达（Structured Representation）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PGM 最根本的贡献是把概率分布的条件独立结构图形化，让一个高维系统变成一张由节点和连边构成的图。图中的每条边都代表某种真实的统计依赖，而缺失的边则代表条件独立假设。换句话说，PGM 不再把概率分布当作一个巨大且难以操作的对象，而是将其拆解成一系列逻辑清晰、可解释的结构部分，每个节点和因子都对应系统中的一个局部规律。通过这种结构化表示，我们可以避免处理指数级的联合空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;因子化分解（Factorized Decomposition）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一旦使用图描述了依赖结构，就可以把意义复杂的联合分布写成许多局部因子的乘积。例如贝叶斯网络的链式分解、马尔可夫随机场的势函数分解、本质上都是沿图结构把复杂的整体拆成小块。这样做的直接收益是计算上从指数复杂度降为多项式，推断不再需要遍历所有可能状态，而是可以通过因子的组合局部地进行计算。因子化让 PGM 变得不仅能表示复杂分布，而且能操作、能更新、能学习。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可计算的推断（Computable Inference）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;结构化和因子化使得许多经典计算在图上可以局部进行。例如消息传递（Belief Propagation）在树上给出精确解，在一般图上给出近似解；变分推断利用结构化因子降低优化维度；MCMC 利用图的条件独立性减少采样依赖。换句话说，PGM 并不是单指一种算法，而是提供了一个框架，使得我们可以根据结构特性选择最适合的推断方法，包括 BP、VI、EP、Gibbs、MH 等。没有结构化，这些算法根本无法在高维空间有效运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;统一解释框架（Unified View of Models）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从 HMM、CRF 到更现代的 VAE 后验近似、能量模型、多模态模型，它们看似属于不同领域，但本质都可以嵌入 PGM 的图结构 + 局部分解 + 局部推断三件套中来理解。PGM 提供的不是一个具体的模型，而是一种 &lt;strong&gt;观看所有概率模型的统一视角&lt;/strong&gt;：只要你需要推断隐变量，只要你的变量之间存在结构性依赖，你都可以把它放进 PGM 框架解析清楚。正因为其统一性，PGM 才成为统计推断、信息论、机器学习之间最重要的桥梁。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总体来看，PGM 的意义就在于把本来难以处理的巨大概率空间拆解成结构良好的局部部分，再让推断算法在这个结构上流动。正是这种结构性，让概率模型不再是一团混乱的高维分布，而变成可以分解、可以优化、可以学习的对象。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;贝叶斯网络基本原理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;关于贝叶斯网络的相关知识可以看下面这个系列视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=33545406&amp;amp;bvid=BV1BW41117xo&amp;amp;cid=58787166&amp;amp;p=2&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;在高维随机系统中，我们真正难以掌握的往往不是概率分布本身，而是分布背后隐含的 &lt;strong&gt;结构关系&lt;/strong&gt; 。多个变量之间既不是完全独立，也不是彼此全都强关联，而是形成了一张稀疏、有向且层次分明的依赖网络。传统的联合分布写法会将这种结构完全掩盖，而 &lt;strong&gt;贝叶斯网络&lt;/strong&gt;（Bayesian Network，简称 BN）正是为了解决这一问题。它通过一个有向无环图（DAG）将高维联合分布展开为一系列局部因子的组合，从而将原本不可处理的全局模型转化为一个结构化、可解释且可推断的系统。&lt;/p&gt;
&lt;p&gt;这种结构化的建模方式带来了显著的优势。依赖关系被明确地写入图结构中，不再埋藏在高维联合分布的符号背后；模型在高维空间中的复杂度得以控制，每个变量只依赖其父节点，从而避免了指数级的参数膨胀；同时，推断也由全局困难的问题转变为可局部分解的操作，使得计算效率显著提高。这些特点共同解释了贝叶斯网络在系统建模、信号处理、因果推断以及图模型研究中长期占据核心地位的原因。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cprobabilistic-graphical-model%5CBN%E7%BB%93%E6%9E%841.jpg&quot; alt=&quot;BN 图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;因子化公式（Factorization Formula）&lt;/h2&gt;
&lt;p&gt;贝叶斯网络由图结构与局部条件分布两部分构成。图决定了谁依赖谁，而每个节点只负责描述自己在父节点给定时的行为。设变量集合为 ${x_1,\ldots,x_n}$ ，它们的联合分布满足著名的因子化公式：&lt;/p&gt;
&lt;p&gt;$$
P(x_1,\ldots,x_n) = \prod_{i=1}^n P(x_i\mid pa(i)),
$$&lt;/p&gt;
&lt;p&gt;其中 $pa(i)$ 表示节点 $i$ 的父集。这个因子化不是随意拆的，而是图结构强制施加的限制：一个节点只看自己的父节点，不需要考虑其他更远的变量如何相互影响。正因为这种局部性，原本指数级增长的联合分布参数量在图的约束下被压缩到了线性规模，或者在稀疏图中变成一个可控的量。整个系统的复杂联合分布，就像是由许多局部的小因子拼装而成，而图结构精确定义了这些因子可以怎样组合。&lt;/p&gt;
&lt;p&gt;在这样的因子化过程中，一些条件独立性自然被 “烘焙” 进系统结构中。例如如果 $x_k$ 的父节点只有 $x_i$ 和 $x_j$ ，那么图直接给出了一个结构化的独立性：&lt;/p&gt;
&lt;p&gt;$$
x_k \perp {x_\ell : \ell \neq i,j,k} \mid (x_i, x_j)
$$&lt;/p&gt;
&lt;p&gt;可以把它理解成每个节点都自带一条独立性声明：它在给定父节点之后，不再受图中其他节点的直接影响。整个网络的条件独立性结构，正是这些局部声明汇总之后的整体结果。&lt;/p&gt;
&lt;h2&gt;独立性结构（Independence Structure）&lt;/h2&gt;
&lt;p&gt;贝叶斯网络的核心价值实际上在于 &lt;strong&gt;将独立性写进图的拓扑结构&lt;/strong&gt; 。图中的路径对应潜在依赖，而 D-Separation 则提供了判断路径是否被阻断的标准。&lt;/p&gt;
&lt;p&gt;若集合 $Z$ 阻断了 $X$ 到 $Y$ 之间的所有有效路径，则有：&lt;/p&gt;
&lt;p&gt;$$
X \perp Y \mid Z
$$&lt;/p&gt;
&lt;p&gt;路径的阻断由三种基本结构决定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;链式结构&lt;/strong&gt;（ $X\to Z\to Y$ ）：条件化 $Z$ 会切断路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分叉结构&lt;/strong&gt;（ $X\leftarrow Z\to Y$ ）：同样因为条件化 $Z$ 而路径中断&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;汇聚结构&lt;/strong&gt;（ $X\to Z\leftarrow Y$ ）：此时反过来，观察 $Z$ 或其后代会激活路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三种结构虽然简单，但它们的组合可以描述极其复杂的依赖与独立性关系，使得 D-Separation 成为整个概率图论的逻辑核心（D-Separation 的具体证明过程可以看上述视频）。&lt;/p&gt;
&lt;p&gt;也正因为图的拓扑直接决定独立性结构，许多推断算法可以只依赖图而不依赖具体参数。&lt;/p&gt;
&lt;h2&gt;局部推断机制（Local Inference Mechanism）&lt;/h2&gt;
&lt;p&gt;当联合分布被图结构拆解为一系列局部因子之后，推断便不再是一个需要处理整个高维空间的全局积分任务，而是转化为沿着图结构逐步进行的局部操作。无论我们选择的推断方法是变量消元（Variable Elimination）、Belief Propagation、Junction Tree，还是更通用的 EP、VI 或基于图结构的 MCMC，它们的核心思想实际上都指向同一个原则：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;全局推断可以通过局部消息的传播实现。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以变量消元为例，边缘化某个变量 $x_k$ 时，我们并不需要访问整个联合分布，而只需要收集所有包含 $x_k$ 的局部因子，把它们相乘，再对 $x_k$ 做一次求和或积分，最后把结果重新作为一个新的因子放回图中。这个过程始终限制在与 $x_k$ 相关的局部区域，不会对其他无关部分产生任何影响。&lt;/p&gt;
&lt;p&gt;在 Belief Propagation 中，这种局部性原则体现得更加直接。每个节点只需要关注与自己相关的因子与邻接边上到来的消息，基于这些局部信息计算并发出新的消息。经过多轮往返传播后，系统逐渐达到一致状态，每个节点都能给出自己的最终 Belief，而这一计算从头到尾都没有触碰到全局联合分布。&lt;/p&gt;
&lt;p&gt;这也意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;推断不依赖全局结构，只依赖本地邻域的图形模式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个节点的计算仅基于它直接可见的因子与邻居消息&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正因为推断可以完全局部化，计算的复杂度最终由图的结构特征决定，尤其是节点度数与图的树宽。稀疏图天然适合快速推断，很多算法都能在多项式时间内完成；而稠密图会显著增加因子大小，使局部操作的代价呈指数增长，导致推断变得困难。因此，PGM 不仅通过图结构表达依赖关系，也通过图的结构本身控制了推断的可计算性。&lt;/p&gt;
&lt;h2&gt;高斯网络（Gaussian Network）&lt;/h2&gt;
&lt;p&gt;当概率图中的节点从离散变量扩展到连续变量时，最符合结构化建模直觉的局部模型就是 &lt;strong&gt;线性–高斯条件分布&lt;/strong&gt; 。它的形式简单、表达能力足够强，而且在推断时能够保持完美的可积性。&lt;/p&gt;
&lt;p&gt;如果节点 $x_i$ 的父集合为 $pa(i)$ ，则它的生成方式可以写成一个线性回归方程：&lt;/p&gt;
&lt;p&gt;$$
x_i = \sum_{j\in pa(i)} w_{ij} x_j + \epsilon_i \qquad \epsilon_i \sim \mathcal{N}(0, \sigma_i^2)
$$&lt;/p&gt;
&lt;p&gt;将所有节点按拓扑顺序堆叠成向量 $x$ ，即可得到整个系统的紧凑矩阵表达：&lt;/p&gt;
&lt;p&gt;$$
x = W x + \epsilon \qquad \epsilon \sim \mathcal{N}(0, D)
$$&lt;/p&gt;
&lt;p&gt;其中 $W$ 的稀疏结构完全由图决定，而 $D$ 为各节点噪声方差构成的对角矩阵。由此可见，高斯贝叶斯网络的结构信息并不隐藏在概率分布中，而是直接体现在矩阵的零–非零模式上。&lt;/p&gt;
&lt;h3&gt;联合分布的闭式表达&lt;/h3&gt;
&lt;p&gt;由于整体是线性的，高斯分布在变换下保持封闭性，因此整个网络天然对应一个多元高斯分布：&lt;/p&gt;
&lt;p&gt;$$
x \sim \mathcal{N}\big(0, (I-W)^{-1} D (I-W)^{-T}\big)
$$&lt;/p&gt;
&lt;p&gt;更重要的是，联合分布的精度矩阵（Precision Matrix）具有如下结构：&lt;/p&gt;
&lt;p&gt;$$
\Lambda = (I-W)^{T} D^{-1} (I-W)
$$&lt;/p&gt;
&lt;p&gt;精度矩阵的稀疏模式与图中的缺边严格对应：若两个节点之间没有直接边，则在精度矩阵对应的位置必然为零。这意味着 &lt;strong&gt;图结构与代数结构实现了精确的一一映射&lt;/strong&gt; ，是高斯网络被广泛使用的关键原因之一。&lt;/p&gt;
&lt;h3&gt;边缘化与条件化&lt;/h3&gt;
&lt;p&gt;高斯模型的强大在于它的 &lt;strong&gt;可闭式推断&lt;/strong&gt; 。给定一个分块高斯分布：&lt;/p&gt;
&lt;p&gt;$$
x=(x_a, x_b) \qquad
x\sim\mathcal{N}\left(
\begin{bmatrix}\mu_a \ \mu_b\end{bmatrix},
\begin{bmatrix}
\Sigma_{aa} &amp;amp; \Sigma_{ab} \
\Sigma_{ba} &amp;amp; \Sigma_{bb}
\end{bmatrix}
\right)
$$&lt;/p&gt;
&lt;p&gt;则条件分布具有完全解析的形式：&lt;/p&gt;
&lt;p&gt;$$
P(x_a|x_b)=\mathcal{N}\big(
\mu_a + \Sigma_{ab}\Sigma_{bb}^{-1}(x_b-\mu_b),
\Sigma_{aa}-\Sigma_{ab}\Sigma_{bb}^{-1}\Sigma_{ba}
\big)
$$&lt;/p&gt;
&lt;p&gt;不需要采样，也不需要近似；所有边缘化和条件化都可以通过矩阵运算直接得到。这使得在连续空间做推断变得异常顺滑。&lt;/p&gt;
&lt;h3&gt;推断的线性代数化&lt;/h3&gt;
&lt;p&gt;高斯贝叶斯网络的推断几乎完全建立在线性代数之上。无论是边缘化、条件化还是消息传递，其核心都可以用矩阵分解或线性组合来实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;边缘化&lt;/strong&gt;：积分等价于做 Schur 补和分块矩阵运算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件化&lt;/strong&gt;：通过 Cholesky 分解等经典线性代数工具完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息传递&lt;/strong&gt;：在 BP 框架中，每条消息都是局部高斯因子的精度与方差的组合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，GBN 将贝叶斯网络的结构化表达能力与高斯模型的解析可计算性自然结合，最终形成一种在连续变量领域既高效又优雅的建模方式。它特别适用于线性系统、动态模型、传感器融合以及任何需要快速解析推断的场景。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;马尔可夫随机场基本原理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;关于马尔科夫随机场的相关知识可以看下面这个系列视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=33545406&amp;amp;bvid=BV1BW41117xo&amp;amp;cid=58881960&amp;amp;p=6&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;在许多真实的高维系统中，变量之间的依赖往往并不表现为明显的 “从原因到结果” 的方向性结构。例如图像中的像素之间并没有严格的先后顺序，它们的相互关系通常更像一种对称的空间耦合；物理系统中的粒子之间也常通过邻近作用能量彼此牵引，而不是通过某个单向的因果链条传播影响；在统计力学、社交网络、序列标注等问题中，节点之间的约束也往往是双向的。面对这种 “无明确方向、但具有强局部相互作用” 的概率结构，贝叶斯网络的有向图就不再是最自然的描述方式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;马尔可夫随机场&lt;/strong&gt;（Markov Random Field，简称 MRF）正是从这种背景下诞生的一类模型。它使用无向图来描述系统中对称的局部依赖关系，将全局联合分布写成若干个势函数（Potential Functions）的组合。通过这种图结构，MRF 不仅能够直观描述复杂系统中的约束模式，还可以将高维概率分布分解为局部能量片段，从而让推断、计算与建模都变得更为可控。&lt;/p&gt;
&lt;p&gt;MRF 的核心思想可以用一句话概括：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;全局行为由局部相互作用决定，邻居决定一切。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;无向图提供了自然的对称结构，而基于团的因子化表达则把系统从 “全局不可处理” 的状态压缩成了 “局部可计算” 的形式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cprobabilistic-graphical-model%5CMRF%E7%BB%93%E6%9E%841.jpg&quot; alt=&quot;MRF 图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;团因子化（Clique Factorization）&lt;/h2&gt;
&lt;p&gt;对于一个无向图 $G=(V,E)$ ，其中节点集合 $V$ 对应随机变量 ${x_1,\dots,x_n}$ ，MRF 假设联合分布可以因子化为若干团上的势函数。设 $\mathcal{C}$ 为所有团的集合，则联合分布写作：&lt;/p&gt;
&lt;p&gt;$$
P(x_1,\dots,x_n) = \frac{1}{Z} \prod_{C\in \mathcal{C}} \psi_C(x_C) \quad \text{where } Z = \sum_x \prod_{C\in \mathcal{C}} \psi_C(x_C)
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\psi_C(x_C)$ 是 &lt;strong&gt;团势函数&lt;/strong&gt; ，反映了团内变量之间的局部相互作用&lt;/li&gt;
&lt;li&gt;$Z$ 是归一化常数，称为 &lt;strong&gt;分区函数（Partition Function）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;势函数不需要满足特定形式，甚至不必归一化，它们仅需要满足非负性。这给予了 MRF 极大的表达能力，例如在图像平滑模型中，邻近像素倾向于取相似值；在 Ising/Potts 模型中，节点倾向于与邻居同态；在物理中的玻尔兹曼分布中，势函数与局部能量直接对应。&lt;/p&gt;
&lt;p&gt;在建模上，团因子的形式是灵活的，你可以指定一对节点之间的势函数，也可以定义三元、四元团来捕捉更复杂的局部结构。&lt;/p&gt;
&lt;h2&gt;邻域结构（Neighborhood Structure）&lt;/h2&gt;
&lt;p&gt;马尔可夫随机场的核心力量来自它的 &lt;strong&gt;局部条件独立结构&lt;/strong&gt; 。对于任意一个节点 $x_i$ ，记其邻居集合为 $\mathcal{N}(i)$ ，MRF 满足经典的 Markov 性质：&lt;/p&gt;
&lt;p&gt;$$
x_i \perp x_{V \setminus ({i} \cup \mathcal{N}(i))} \mid x_{\mathcal{N}(i)}
$$&lt;/p&gt;
&lt;p&gt;这句话的含义可以简单概括为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;只要知道它的邻居，节点与所有其他非邻居节点就完全独立。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MRF 的独立性结构与贝叶斯网络有着根本性的不同：BN 的独立性由方向性带来的路径阻断（D-Separation）决定，需要分析整个图的有向路径。而 MRF 的独立性则更加直接，它完全由无向图的邻域关系定义——图上没有边，就意味着没有直接相互作用，也没有额外的依赖需要考虑。&lt;/p&gt;
&lt;p&gt;这种基于邻域的局部性使得 MRF 的推断天然具备可分解性。无论是在计算条件概率、更新节点状态，还是进行 Gibbs 采样，所有操作都严格局限在局部邻域内部，不需要访问全局结构。这也使得势函数变得高度可解释：图中每个团的势函数只描述该团内部变量的相互作用，而不同团之间的关系则完全由图结构决定。&lt;/p&gt;
&lt;h2&gt;玻尔兹曼分布（Boltzmann Distribution）&lt;/h2&gt;
&lt;p&gt;在马尔可夫随机场（MRF）中，我们常将原本的势函数表示方式改写为能量形式：&lt;/p&gt;
&lt;p&gt;$$
\psi_C(x_C) = \exp\big(-E_C(x_C)\big),
$$&lt;/p&gt;
&lt;p&gt;此时整个联合分布的表达式变为经典的 &lt;strong&gt;玻尔兹曼分布（Boltzmann Distribution）&lt;/strong&gt; 形式：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \frac{1}{Z} \exp\Big( - \sum_{C\in\mathcal{C}} E_C(x_C) \Big).
$$&lt;/p&gt;
&lt;p&gt;之所以强调这种能量式分布，是因为它直接继承了玻尔兹曼分布的两个核心特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;能量越低，概率越大&lt;/strong&gt;：低能量状态在全局分布中更容易出现，与物理系统趋向稳定的直觉一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;概率完全由能量差决定&lt;/strong&gt;：许多优化、采样与推断方法（如模拟退火、Gibbs 采样）都只依赖能量差，而非绝对概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 Gibbs 采样为例，其局部更新完全基于玻尔兹曼形式的局部能量：&lt;/p&gt;
&lt;p&gt;$$
P(x_i \mid x_{\mathcal{N}(i)})
\propto \psi_i(x_i),\psi_{\mathcal{C}(i)}(x_i , x_{\mathcal{N}(i)}),
$$&lt;/p&gt;
&lt;p&gt;其中 $\mathcal{C}(i)$ 表示包含节点 $i$ 的所有对偶团，更新过程只需考虑其邻域结构。&lt;/p&gt;
&lt;p&gt;因此，MRF 的推断本质上就是在玻尔兹曼分布上进行局部能量优化（或随机采样），整个系统的行为都遵循 &lt;strong&gt;能量主导概率&lt;/strong&gt; 的规律。&lt;/p&gt;
&lt;h2&gt;高斯随机场（Gaussian Random Field）&lt;/h2&gt;
&lt;p&gt;当随机变量是连续的，并且联合分布为高斯分布时，MRF 会出现一个极为美妙的性质：&lt;strong&gt;条件独立性与精度矩阵的稀疏性完全对应&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果假设 $x \sim \mathcal{N}(0, \Sigma)$ ，其中 $\Lambda = \Sigma^{-1}$ ，那我们就可以得到：&lt;/p&gt;
&lt;p&gt;$$
\Lambda_{ij} = 0 \iff x_i \perp x_j \mid x_{{1,\dots,n}\setminus{i,j}}
$$&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;图中没有边等价于精度矩阵对应位置为零&lt;/strong&gt; 。这意味着图结构直接给出了联合分布在代数结构上的稀疏模式。因此 GMRF 在图像恢复、空间统计（如 Gaussian Process 的稀疏近似）、信号处理等领域极为常用。&lt;/p&gt;
&lt;p&gt;对于任意分块高斯：&lt;/p&gt;
&lt;p&gt;$$
x=
\begin{bmatrix}
x_a \ x_b
\end{bmatrix}
\sim \mathcal{N}\Big(
\begin{bmatrix}
\mu_a \ \mu_b
\end{bmatrix},
\begin{bmatrix}
\Sigma_{aa} &amp;amp; \Sigma_{ab}\
\Sigma_{ba} &amp;amp; \Sigma_{bb}
\end{bmatrix}
\Big)
$$&lt;/p&gt;
&lt;p&gt;其条件分布为：&lt;/p&gt;
&lt;p&gt;$$
P(x_a|x_b) = \mathcal{N}\big(
\mu_a + \Sigma_{ab}\Sigma_{bb}^{-1}(x_b-\mu_b),
\Sigma_{aa}-\Sigma_{ab}\Sigma_{bb}^{-1}\Sigma_{ba}
\big)
$$&lt;/p&gt;
&lt;p&gt;所有运算都退化为线性代数，非常便于推断。&lt;/p&gt;
&lt;h2&gt;推断机制（Inference Mechanism）&lt;/h2&gt;
&lt;p&gt;MRF 的推断思路与贝叶斯网络有着相同的核心原则：&lt;strong&gt;全局推断通过局部因子之间的消息传递实现&lt;/strong&gt; 。不同的是，MRF 的图是无向的，因此推断通常基于团分解或因子图展开，使局部性在结构上更加直接。整体上，MRF 的推断可分为精确推断与近似推断两类。&lt;/p&gt;
&lt;h3&gt;精确推断&lt;/h3&gt;
&lt;p&gt;当图结构较为简单（如树结构或树宽较小的图）时，可以使用精确推断方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Belief Propagation&lt;/strong&gt;（在无环图上等价于精确推断）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Junction Tree 方法&lt;/strong&gt;（先三角化图，再在团树上做消息传递）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在树形结构上，BP 可以得到节点的精确边缘分布；而在一般 MRF 上，需要先将原图转化为连接树，从而保证消息传递在更大的团级别仍保持正确性。
不过这一过程往往会增大团大小，因此精确推断通常只适用于图比较稀疏或具有良好结构的情况。&lt;/p&gt;
&lt;h3&gt;近似推断&lt;/h3&gt;
&lt;p&gt;面对稠密图或高维图结构，精确推断的代价会迅速变得不可接受，此时就需要借助近似推断方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MH（Metropolis-Hastings）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VI（Variational Inference）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EP（Expectation Propagation）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在能量形式的 MRF 中，Gibbs 采样尤其自然，因为每次采样只需要观察节点的邻域，而无需访问整个图的状态。其更新规则为：&lt;/p&gt;
&lt;p&gt;$$
P(x_i,|,x_{\mathcal{N}(i)}) \propto \psi_i(x_i)\prod_{j\in\mathcal{N}(i)} \psi_{ij}(x_i, x_j)
$$&lt;/p&gt;
&lt;p&gt;这与物理系统中 “逐点能量最小化” 的直觉高度一致。无论是连续的 GMRF，还是离散的图像 MRF，这种局部更新方式都使得推断具有极高的可扩展性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题思考&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;贝叶斯网络为什么要用从属关系建模？马尔可夫随机场为什么要用团来建模？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;贝叶斯网络之所以以从属关系（父 → 子）来建模，本质上是因为联合分布最自然、最基础的拆解方式就是链式法则。链式法则本身带有方向性，每一个变量都写成 “在前面所有变量条件下” 的形式。而贝叶斯网络做的事情，就是利用图结构告诉我们：一个变量真正依赖的不是所有前序变量，而只是其中极少的父节点。方向结构刚好表达了 “谁决定谁” “谁提供信息给谁” 这一点。于是条件独立的削减就自然变成了父子依赖。换句话说，BN 的有向边不是人为设定，而是链式分解在图结构约束下的简化结果：&lt;strong&gt;方向性 + 条件独立性 = 从属关系建模&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;而马尔可夫随机场则从完全不同的路径进入：它不从链式法则出发，而是从联合分布的对称交互结构出发。无向图中最基本的独立关系不是 “谁依赖谁” ，而是 “在给定邻居之后，一个节点与其他所有节点独立” 。这种独立性无法写成方向性的条件概率项，它只能通过 “哪些变量必须一起出现” 来定义局部依赖。Hammersley–Clifford 定理指出，所有满足无向图条件独立性的分布，都必须分解为 &lt;strong&gt;极大团上的势函数&lt;/strong&gt; 乘积。没有方向，也没有父子关系，只有 “哪些节点必须共同参与一个因子” 这一结构。因此 MRF 的构件自然就是团：&lt;strong&gt;无向条件独立性 + 相容性结构 = 团势函数建模&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;两者最后都实现了高维分布的可控因子化，但路径完全不同：BN 用 “依赖方向” 来削减链式法则的条件集合；MRF 用 “局部相容性” 来表达无向图的独立结构。前者更接近生成式建模的直觉，后者更像物理能量系统的平衡描述。两种模型背后代表的，是概率论中两个完全不同的 “拆解联合分布” 的哲学。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献&lt;/h1&gt;
&lt;h2&gt;贝叶斯网络&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/%40segunemmanuel46/introduction-to-bayesian-networks-2b62b4d35a52&quot;&gt;Introduction to Bayesian Networks&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jmaasch.github.io/pgm/&quot;&gt;概率图模型简明教程&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://mathigon.org/course/bayesian-inference-and-graphical-models/introduction&quot;&gt;【Mathigon】贝叶斯推断和图模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://liangliangzhuang.github.io/MachineLearningNote/%E9%AB%98%E6%96%AF%E7%BD%91%E7%BB%9C.html&quot;&gt;高斯网络详细讲解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/463764463&quot;&gt;ML白板推导18：高斯网络&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;马尔科夫随机场&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bookstack.cn/read/huaxiaozhuan-ai/spilt.3.a1c8cb11a2e246b2.md&quot;&gt;AI算法工程师手册：马尔可夫随机场&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_46836893/article/details/144287338&quot;&gt;【高中生讲机器学习】29. 马尔可夫随机场&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_40507857/article/details/110164691&quot;&gt;从贝叶斯理论到马尔可夫随机场（MRF）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://alex-mcavoy.github.io/mathematics/mathematical-statistics/a8049182.html&quot;&gt;【Alex_McAvoy】马尔可夫随机场&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://leimao.github.io/blog/Markov-Random-Field-VS-Conditional-Random-Field/&quot;&gt;马尔可夫随机场和条件随机场&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基础算法】第二节：降维算法</title><link>https://xingguang641.com/posts/dimensionality-reduction/dimensionality-reduction/</link><guid isPermaLink="true">https://xingguang641.com/posts/dimensionality-reduction/dimensionality-reduction/</guid><description>介绍机器学习常见的算法</description><pubDate>Sun, 16 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;降维算法背景介绍&lt;/h1&gt;
&lt;p&gt;在现代机器学习中，我们经常要面对维度极高的数据。高维不仅让计算成本迅速膨胀，更重要的是会引发所谓的 &lt;strong&gt;维度诅咒&lt;/strong&gt;：维度越高，数据在空间中越稀疏，距离与密度等直观概念逐渐失效，噪声与冗余特征甚至会压过真正的结构，使得许多算法变得不稳定，甚至完全失去效果。为了缓解这些问题，人们很早便尝试对数据进行降维。&lt;/p&gt;
&lt;p&gt;从最初依赖经验的手工特征筛选，到基于方差或简单投影的粗略方法，再到后来出现的经典多维尺度（MDS）、Sammon 映射，以及 Isomap、LLE 等流形学习技术，研究者不断追求一种既能有效压缩数据，又能尽量保留关键结构的通用工具。这些方法在理念上确实十分优美，试图从几何或拓扑层面理解数据本身的结构规律。然而，它们往往对噪声敏感、对参数高度依赖，且在大规模数据下易遭遇计算瓶颈，因此难以真正成为工业和研究实践中的主力方案。&lt;/p&gt;
&lt;p&gt;真正让降维技术走向成熟应用的是 &lt;strong&gt;主成分分析方法&lt;/strong&gt;（Principal Component Analysis，简称 PCA）。PCA 从统计与线性代数的基本原理出发，通过寻找使数据方差最大的投影方向来构建低维子空间。它简单、稳定、效率高，还具备良好的可解释性，因此迅速成为数据预处理、降噪与压缩的标准步骤。&lt;/p&gt;
&lt;p&gt;然而，随着数据规模和结构复杂度不断增长，人们逐渐意识到纯线性方法无法充分揭示现实数据中普遍存在的弯曲流形结构，因此更灵活的非线性降维方法变得必要。在一系列流形学习方法逐渐暴露短板之后， &lt;strong&gt;统一流行逼近与投影&lt;/strong&gt;（Uniform Manifold Approximation and Projection，简称 UMAP）应运而生。UMAP 融合了流形学习与拓扑学的思想，先在高维空间构建模糊化的邻域图，再在低维空间通过优化保留这些邻域关系，从而在速度、可扩展性与结构保持之间取得了极佳的平衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cdimensionality-reduction%5C%E9%99%8D%E7%BB%B4%E7%AE%97%E6%B3%951.jpg&quot; alt=&quot;降维算法图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在，我们就来详细讲解一下这两类方法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;主成分分析基本原理&lt;/h1&gt;
&lt;p&gt;PCA 最早由 Karl Pearson 在 1901 年提出，起源于一个非常直观的问题：高维数据中常有大量相关和冗余特征，如果能用更少的、不相关的新特征来概括原始结构，那么数据将更容易理解、可视化与处理。PCA 的核心思路来自这样一个几何直觉：把数据投影到某个方向上，如果投影后的分布越 “拉得开” ，说明这个方向包含越多关于数据真实结构的信息；相反，分布很窄说明主要是噪声。因此我们想找到所有投影方向中 “信息量最大” 的那些方向，并以它们作为新的坐标轴来表示数据。&lt;/p&gt;
&lt;p&gt;令人惊讶的是，这个纯几何的问题最终与统计中的协方差矩阵特征值分解完全对应。数据变化最大的方向正是协方差矩阵最大特征值对应的特征向量；按特征值大小依次排列的特征向量，就构成了最能保留原始信息的低维子空间。借由这个数学基础，PCA 成为了一个简单、稳定、计算高效且极具可解释性的降维方法，被广泛应用于噪声过滤、特征压缩、可视化与预处理等几乎所有数据分析流程中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cdimensionality-reduction%5C%E4%B8%BB%E6%88%90%E5%88%86%E5%88%86%E6%9E%901.jpg&quot; alt=&quot;主成分分析图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;优化问题（Optimization Problem）&lt;/h2&gt;
&lt;p&gt;从前面的讨论可知，我们希望找到一个或若干方向，使得数据在这些方向上的投影方差最大。方差越大，数据在该方向上分布得越 “拉得开” ，说明包含的信息越多。&lt;/p&gt;
&lt;p&gt;给定数据矩阵 $X$（每行一个样本），将其投影到单位向量 $w$ 上有：&lt;/p&gt;
&lt;p&gt;$$
z = Xw
$$&lt;/p&gt;
&lt;p&gt;若数据已做中心化处理，则投影后的方差为：&lt;/p&gt;
&lt;p&gt;$$
\text{Var}(z) = \frac{1}{n}z^{\rm T}z = \frac{1}{n} (Xw)^{\rm T}(Xw) = w^{\rm T}(\frac{1}{n} X^{\rm T}X)w = w^{\rm T}Sw
$$&lt;/p&gt;
&lt;p&gt;其中 $\displaystyle S = \frac{1}{n} X^{\rm T}X$ 即为样本协方差矩阵。&lt;/p&gt;
&lt;p&gt;为了避免通过简单拉长向量来无限放大方差，我们需要对 $w$ 加以约束，使其长度为 1。于是便得到 PCA 的核心最优化问题：&lt;/p&gt;
&lt;p&gt;$$
\max_{w} w^T S w \quad \text{s.t.} |w| = 1
$$&lt;/p&gt;
&lt;p&gt;即在所有单位向量中寻找能最大化投影方差的方向。&lt;/p&gt;
&lt;p&gt;我们已经知道 PCA 核心的问题是一个带约束的二次型优化问题。因此构造拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
L(w, \lambda) = w^{\rm T} S w - \lambda (w^{\rm T} w - 1)
$$&lt;/p&gt;
&lt;p&gt;对 $w$ 求梯度并令其为零：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial w} = 2Sw - 2\lambda w = 0 \quad \Rightarrow \quad Sw = \lambda w
$$&lt;/p&gt;
&lt;p&gt;这表明任何使 $w^{\rm T}Sw$ 取得最值的向量 $w$ ，必然是协方差矩阵 $S$ 的 &lt;strong&gt;特征向量&lt;/strong&gt; ，其中 $\lambda$ 即对应的 &lt;strong&gt;特征值&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;将方程代回目标函数可得：&lt;/p&gt;
&lt;p&gt;$$
w^{\rm T} Sw = w^{\rm T} (\lambda w) = \lambda (w^{\rm T} w) = \lambda
$$&lt;/p&gt;
&lt;p&gt;由于约束条件 $|w| = 1$ ，可知投影方差等于其对应的特征值。&lt;/p&gt;
&lt;h2&gt;多主成分分析（Multivariate PCA）&lt;/h2&gt;
&lt;p&gt;在得到第一个主成分之后，我们通常希望继续提取更多方向，以捕获数据中尚未表达的信息。然而，如果没有额外约束，后续方向可能会与已有主成分表示相同的信息，从而产生冗余。&lt;/p&gt;
&lt;p&gt;因此，第二个主成分的优化问题必须在 &lt;strong&gt;既能最大化投影方差&lt;/strong&gt; 又 &lt;strong&gt;与第一个主成分正交&lt;/strong&gt; 的条件下进行。形式化地表示为：&lt;/p&gt;
&lt;p&gt;$$
\max_{w_2} w_2^{\rm T} S w_2 \quad \text{s.t.} |w_2| = 1 \quad w_1^{\rm T} w_2 = 0
$$&lt;/p&gt;
&lt;p&gt;推广到第 $K$ 个主成分：&lt;/p&gt;
&lt;p&gt;$$
\max_{w_k} w_k^{\rm T} S w_k \quad \text{s.t.} |w_k| = 1 \quad w_i^{\rm T} w_k = 0, , i = 1, \ldots, k-1
$$&lt;/p&gt;
&lt;p&gt;根据拉格朗日优化推导可得：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所有主成分方向均为协方差矩阵的特征向量，并按特征值从大到小依次排列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于协方差矩阵 $S$ 是对称半正定矩阵，根据线性代数定理：它具有一组正交特征向量，这些向量构成一组正交基。于是可表示为特征值分解：&lt;/p&gt;
&lt;p&gt;$$
S = U \Lambda U^{\rm T}
$$&lt;/p&gt;
&lt;p&gt;其中 $U = [w_1, w_2, \dots, w_d]$ 为正交矩阵，$\Lambda = \mathrm{diag}(\lambda_1, \lambda_2, \dots, \lambda_d)$ 且满足：&lt;/p&gt;
&lt;p&gt;$$
\lambda_1 \geq \lambda_2 \geq \cdots \geq \cdots \geq \lambda_d \geq 0
$$&lt;/p&gt;
&lt;p&gt;选取前 $K$ 个主成分组成线性空间中的投影矩阵：&lt;/p&gt;
&lt;p&gt;$$
W_k = [w_1, w_2, \ldots, w_k]
$$&lt;/p&gt;
&lt;p&gt;则降维后数据在新坐标下的表示为：&lt;/p&gt;
&lt;p&gt;$$
Z = W_k^{\rm T} X
$$&lt;/p&gt;
&lt;p&gt;这样得到的特征正交且不相关，信息量也依次递减，可以最大化保留原始数据的主要结构。&lt;/p&gt;
&lt;h2&gt;方差解释率（Explained Variance Ratio）&lt;/h2&gt;
&lt;p&gt;在 PCA 中，每个特征值 $\lambda_k$ 表示数据在对应主成分方向上的方差，也就衡量了该主成分 &lt;strong&gt;对原始数据信息的贡献&lt;/strong&gt; 。为了评估前 $K$ 个主成分能保留多少信息，我们引入以下指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;单个主成分的方差解释率（Explained Variance Ratio）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
r_k = \frac{\lambda_k}{\sum_{i=1}^{d} \lambda_i}
$$&lt;/p&gt;
&lt;p&gt;用于描述第 $k$ 个主成分单独保留的原始信息比例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;累积方差解释率（Cumulative Explained Variance）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
R_k = \sum_{i=1}^{K} r_i = \frac{\sum_{i=1}^{k} \lambda_i}{\sum_{i=1}^{d} \lambda_i}
$$&lt;/p&gt;
&lt;p&gt;用于衡量选取前 $K$ 个主成分后总共保留的信息量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么在实际应用中，我们应如何选择主成分数量 $K$ 呢？&lt;strong&gt;碎石图（Scree Plot）&lt;/strong&gt; 是最常见、最直观的方法：将特征值按降序绘制折线图，当曲线从陡降转为平缓处出现 “拐点” 时，该位置通常对应一个合适的 $K$ 值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下图为使用 Matplotlib 所绘制的碎石图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cdimensionality-reduction%5C%E7%A2%8E%E7%9F%B3%E5%9B%BE1.jpg&quot; alt=&quot;碎石图图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于上面这幅图，我们可以选择 2 或者 3 作为主成分数量。&lt;/p&gt;
&lt;h2&gt;奇异值分解&lt;/h2&gt;
&lt;p&gt;除了传统的基于协方差矩阵的特征值分解，PCA 还可以通过 &lt;strong&gt;奇异值分解&lt;/strong&gt;（Singular Value Decomposition，简称 SVD）来实现。假设我们将中心化后的数据矩阵记为 $X$ ，SVD 可以将其分解为&lt;/p&gt;
&lt;p&gt;$$
X = U \Sigma V^{\rm T}
$$&lt;/p&gt;
&lt;p&gt;其中 $U$ 和 $V$ 分别是左、右奇异向量矩阵，$\Sigma$ 是对角奇异值矩阵。通过这个分解可以发现，PCA 的主成分方向实际上就是 $V$ 的列向量，而对应的方差由奇异值的平方除以样本数给出：&lt;/p&gt;
&lt;p&gt;$$
\lambda_i = \frac{1}{n} \sigma_i^2
$$&lt;/p&gt;
&lt;p&gt;换句话说，SVD 给出了与协方差矩阵特征分解完全一致的结果，但在数值计算上更加稳定，也适合处理高维或样本量较小的情况。&lt;/p&gt;
&lt;p&gt;借助 SVD，我们不仅能够快速获得主成分方向，还可以直接得到降维后的数据表示。将数据投影到前 $K$ 个主成分上，可以写作：&lt;/p&gt;
&lt;p&gt;$$
Z = X V_k = U_k \Sigma_k
$$&lt;/p&gt;
&lt;p&gt;其中左奇异向量 $U_k$ 给出了样本在新坐标系中的坐标，而奇异值 $\Sigma_k$ 对应的缩放反映了各主成分的方差大小。这样，PCA 的理论与 SVD 的计算方法在本质上是等价的，但通过 SVD 可以更加直观地理解主成分的 “信息量” ，并且在实际应用中操作更简便。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面是 SVD 的详细介绍视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=112996076490296&amp;amp;bvid=BV1ExWxesEVf&amp;amp;cid=500001656999667&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;统一流形逼近与投影基本原理&lt;/h1&gt;
&lt;p&gt;UMAP 是一种现代的非线性降维方法，由 McInnes 等人在 2018 年提出，其核心思想来源于一个直观的问题：高维数据往往位于某个低维流形上，如果我们能找到一种方法，将数据映射到低维空间，同时尽量保留局部结构和全局拓扑，那么数据的可视化与分析将更加直观和有效。&lt;/p&gt;
&lt;p&gt;UMAP 的几何直觉是这样的：数据在高维空间中往往形成复杂的曲面或 “皱折” ，相互靠近的点表示它们在原始空间中相似，而远离的点表示它们差异较大。UMAP 通过构建高维邻域图来近似数据的潜在流形，然后在低维空间中重建这个图，使得高维的相似关系尽量在低维中保持不变。换句话说，它找到一种低维表示，使得原本局部紧密的点在低维投影后仍然紧密，而不相似的点被拉开，从而最大限度保留数据的局部和全局结构信息。&lt;/p&gt;
&lt;p&gt;正是这种结合了 &lt;strong&gt;流形学习&lt;/strong&gt; 和 &lt;strong&gt;概率投影优化&lt;/strong&gt; 的思想，使得 UMAP 成为一种强大的降维工具。它不仅可以用于数据可视化，还可作为高维数据预处理的手段，广泛应用于生物信息学、图像处理、自然语言处理等领域。与传统方法（如 PCA）相比，UMAP 更适合处理非线性结构的数据，同时在保持局部邻域关系的同时，也能较好地反映全局拓扑。&lt;/p&gt;
&lt;h2&gt;高维邻域图构建（Neighborhood Graph Construction）&lt;/h2&gt;
&lt;p&gt;UMAP 的第一步是 &lt;strong&gt;在高维空间中刻画数据的局部结构&lt;/strong&gt; ，即构建邻域图（Neighborhood Graph）。该图的核心目的是明确哪些点在高维空间中相似，哪些点不相似，从而为低维映射提供基础。&lt;/p&gt;
&lt;p&gt;给定数据矩阵 $X \in \mathbb{R}^{n \times d}$ ，对于每个点 $x_i$ ，找到其在高维空间中的 $K$ 个最近邻点 $N(x_i)$ ：&lt;/p&gt;
&lt;p&gt;$$
N(x_i) = {x_j | x_j \text{是距离 } x_i \text{ 最近的 } k \text{ 个点} }
$$&lt;/p&gt;
&lt;p&gt;这些近邻点反映了数据的局部相似关系，因此在低维空间中也应尽量保持靠近，以保留局部结构。然而，UMAP 注意到不同点的局部密度可能差异较大：有些区域点稀疏，有些区域点密集。为了公平衡量每个点的邻域相似性，引入了 &lt;strong&gt;局部最小距离 $\rho_i$&lt;/strong&gt; 和 &lt;strong&gt;自适应平滑参数 $\sigma_i$&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;局部最小距离 $\rho_i$ 定义为点 $x_i$ 与其最近邻之间的最小距离：&lt;/p&gt;
&lt;p&gt;$$
\rho_i = \min_{j \in N(x_i)} \text{dist}(x_i, x_j)
$$&lt;/p&gt;
&lt;p&gt;自适应平滑参数 ​$\sigma_i$ 控制邻域权重的衰减速度，用来调节每个点的局部尺度，以适应密度不同的区域。密集区域的点 ​$\sigma_i$ 较小，权重衰减快，只保留最近邻的强关系；稀疏区域的点 ​$\sigma_i$ 较大，权重衰减慢，可以保留更多邻居信息。通过这种方式，每个点的邻域权重总和大致相等，从而公平衡量局部相似性（公式中取 $max$ 是因为防止过近的点在浮点计算中造成意外）：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j \in N(x_i)} \exp \left( - \frac{\max(0, \text{dist}(x_i, x_j) - \rho_i)}{\sigma_i} \right) = \log_2(k)
$$&lt;/p&gt;
&lt;p&gt;可以直观理解为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每个点用自己的 “尺子” 去衡量周围邻居的距离，密集区用小尺子，稀疏区用大尺子，从而保证每个点都公平评估谁是自己的邻居。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;利用 ​$\rho_i$ 和 $\sigma_i$ ，点对 $x_i$ 和 $x_j$ 的高维相似度权重定义为：&lt;/p&gt;
&lt;p&gt;$$
w_{ij} = \exp \left( - \frac{\max(0, \text{dist}(x_i, x_j) - \rho_i)}{\sigma_i} \right)
$$&lt;/p&gt;
&lt;p&gt;将所有点和权重组合，就得到一个稀疏加权邻域图 $G = (V, E, W)$ ，它完整刻画了高维数据的局部结构，为后续的低维映射提供基础。&lt;/p&gt;
&lt;h2&gt;低维映射与概率优化（Low-Dimensional Mapping）&lt;/h2&gt;
&lt;p&gt;在构建好高维邻域图 $G = (V, E, W)$ 后，UMAP 的下一步是 &lt;strong&gt;将高维结构映射到低维空间&lt;/strong&gt; 。核心思想是：找到低维表示，使得原本高维空间中相似的点在低维空间中仍然靠近，而不相似的点尽量分开，从而最大限度保留局部和全局拓扑结构。&lt;/p&gt;
&lt;p&gt;设低维嵌入为矩阵 $Y \in \mathbb{R}^{n \times d&apos;}$ ，其中 $d&apos; \ll d$ 。UMAP 通过概率模型衡量低维空间中点的相似性：&lt;/p&gt;
&lt;p&gt;$$
p_{ij} = \frac{1}{1 + a|y_i - y_j|^{2b}}
$$&lt;/p&gt;
&lt;p&gt;其中 $y_i$ 、$y_j$ 是点 $x_i$ 、$x_j$ 在低维空间中的映射，超参数 $a$ 和 $b$ 控制距离与相似度的非线性关系，使得近邻距离对应较高相似度，而远处距离相似度迅速衰减。&lt;/p&gt;
&lt;p&gt;为了让低维嵌入尽量保持高维结构，UMAP 使用交叉熵作为优化目标，将低维相似度 $p_{ij}$ 与高维邻域权重 $w_{ij}$ 对齐：&lt;/p&gt;
&lt;p&gt;$$
L(Y) = \sum_{(i,j) \in E} w_{ij} \log \frac{w_{ij}}{p_{ij}} + (1 - w_{ij}) \log \frac{1 - w_{ij}}{1 - p_{ij}}
$$&lt;/p&gt;
&lt;p&gt;通过梯度下降等优化方法最小化 $L(Y)$ ，低维点不断调整位置，使得高权重的点对在低维空间尽量靠近，而低权重或非邻近点对被拉远。&lt;/p&gt;
&lt;p&gt;最终得到的低维嵌入 $Y$ 能够同时保留高维数据的局部邻域关系和一定的全局拓扑结构。这也是 UMAP 在可视化非线性高维数据时，能够清晰显示群集和流形结构的原因。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果对上述讲解仍有不理解的话可以观看下面这个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113389921703882&amp;amp;bvid=BV1dpStYvEh8&amp;amp;cid=26517833018&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题思考&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;主成分分析只是单纯的寻找方差最大的投影方向，理论上应该适合所有数据，但为什么说主成分分析只适合线性数据？&lt;/p&gt;
&lt;p&gt;PCA 的目标是：找到使投影后 &lt;strong&gt;方差最大&lt;/strong&gt; 的方向。&lt;/p&gt;
&lt;p&gt;数学上，它等价于最大化：&lt;/p&gt;
&lt;p&gt;$$
\text{Var}(Xw) = w^{\rm T}Sw
$$&lt;/p&gt;
&lt;p&gt;乍一看，这与数据形状似乎毫无关系，只要沿着方差最大的方向去投影，不就保留了最多的信息吗？既然方差越大表示差异性越强，理论上应该适合各种数据才对。但问题恰恰就出在这里。&lt;/p&gt;
&lt;p&gt;PCA 实际上只能找到 &lt;strong&gt;线性的投影方向&lt;/strong&gt; 。协方差矩阵仅仅描述了数据在全局范围内的 &lt;strong&gt;线性结构&lt;/strong&gt; ，它能刻画的只有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点与点之间的 &lt;strong&gt;直线距离&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;特征之间的 &lt;strong&gt;线性相关性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据整体的 &lt;strong&gt;线性伸展趋势&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 PCA 默认了一个非常强的假设：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据变化最大的方向等于数据结构中最重要的方向，且这种变化沿着一条 &lt;strong&gt;直线&lt;/strong&gt; 展开&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但对于流形结构、弯曲结构等 &lt;strong&gt;非线性数据&lt;/strong&gt; 来说，最大方差方向可能并不能反映真正有意义的几何结构。数据看似分散，却可能只是沿着某条弯曲的低维空间分布，这时 PCA 的线性投影就无法有效保留其内部结构信息。&lt;/p&gt;
&lt;p&gt;来看一个最典型的例子：三维空间中的螺旋结构。如果数据点沿着一条三维螺旋线分布，相似性是沿着曲线逐渐变化的，局部邻域也具有连续且弯曲的形式。然而 PCA 在寻找最大方差方向时，只会选择让整体伸展程度最大的那条直线作为投影轴。这样一来，低维投影会直接把螺旋压扁成一条直线，原本邻近的点可能被拉开，不相邻的点却挤在一起，曲线结构消失殆尽。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献&lt;/h1&gt;
&lt;h2&gt;主成分分析&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://algo.ac.cn/archives/shen-ru-qian-chu-zhu-cheng-fen-fen-xi-pca-cong-yuan-li-dao-tui-dao-de-wan-quan-zhi-nan&quot;&gt;深入浅出主成分分析（PCA）：从原理到推导的完全指南&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.math.pku.edu.cn/teachers/lidf/course/mvr/mvrnotes/html/_mvrnotes/mvr-pca.html&quot;&gt;多元统计分析讲义——主成分分析&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_43819566/article/details/113800120&quot;&gt;主成分分析(PCA)原理详解及案例分析&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/m0_64087341/article/details/143991155&quot;&gt;机器学习算法之主成分分析法（PCA）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/pinard/p/6239403.html&quot;&gt;主成分分析（PCA）原理总结&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://leemeng.tw/essence-of-principal-component-analysis.html&quot;&gt;世上最生動的 PCA：直觀理解並應用主成分分析&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;统一流形逼近与投影&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/sinat_28461591/article/details/147654346&quot;&gt;三大主流降维方法详解与应用实战&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pair-code.github.io/understanding-umap/&quot;&gt;Understanding UMAP&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.datacamp.com/tutorial/understanding-umap-guide-to-dimensionality-reduction&quot;&gt;理解 UMAP：降维综合指南&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://topos.institute/blog/2024-04-05-understanding-umap/&quot;&gt;Topos Institute: Understanding UMAP&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第十一节：高斯过程分类</title><link>https://xingguang641.com/posts/regression-model/gaussian-process-classification/</link><guid isPermaLink="true">https://xingguang641.com/posts/regression-model/gaussian-process-classification/</guid><description>介绍机器学习常见的模型</description><pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;高斯分类基本原理&lt;/h1&gt;
&lt;p&gt;在传统的机器学习中， &lt;strong&gt;逻辑回归&lt;/strong&gt; 是最基础、最常用的分类模型之一。它利用线性决策边界，并结合 sigmoid 函数，将输入映射为类别概率，从而解决二分类任务。然而在许多真实场景中，数据往往呈现出 &lt;strong&gt;高度非线性、噪声强、决策边界复杂多样&lt;/strong&gt; 的特征。简单的线性决策面显然难以刻画这种复杂结构，也无法准确反映模型对自身预测的信心。&lt;/p&gt;
&lt;p&gt;正因如此，我们需要引入 &lt;strong&gt;概率建模&lt;/strong&gt; 的思想：不仅要输出分类结果，还要评估预测背后的 &lt;strong&gt;不确定性&lt;/strong&gt; 。在上一篇文章中，我们通过高斯过程回归（GPR）已经见识过这种 “既预测函数，又衡量不确定性” 的能力。而在分类任务中，我们同样希望拥有类似的建模方式。&lt;/p&gt;
&lt;p&gt;在这样的需求驱动下， &lt;strong&gt;高斯过程分类&lt;/strong&gt;（Gaussian Process Classification，简称 GPC）应运而生。它和 GPR 一样属于 &lt;strong&gt;非参数贝叶斯方法&lt;/strong&gt; ，无需人为设定决策边界的具体形式，而是通过为一个 “潜在函数” 建立高斯过程先验，让模型自动学习灵活、非线性的分类规则。同时可以为每一次预测提供严谨的概率解释与不确定性度量。&lt;/p&gt;
&lt;p&gt;不过要真正理解高斯过程分类背后的思想，我们需要从它的 “简化形式” 开始构建直觉 ———— 这便是 &lt;strong&gt;贝叶斯逻辑回归（Bayesian Logistic Regression）&lt;/strong&gt;。可以将 GPC 看作是 “将贝叶斯逻辑回归扩展到无限维函数空间” 的自然结果：当我们把线性模型替换为高斯过程先验，逻辑回归便以一种更强大、更优雅的方式被泛化到非线性情形之中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cregression-model%5C%E9%AB%98%E6%96%AF%E8%BF%87%E7%A8%8B%E5%88%86%E7%B1%BB1.jpg&quot; alt=&quot;高斯过程回归分类&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;贝叶斯逻辑回归&lt;/h2&gt;
&lt;p&gt;传统逻辑回归通常采用最大似然（MLE）学习参数 $w$ ，但这种做法存在两显著个局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无法表达参数不确定性&lt;/strong&gt; ：模型给出的结果是单一的、确定的权重向量 $w$ ，无法告诉我们 “模型是否确定” 这一判断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容易过拟合&lt;/strong&gt; ：在小数据集或高维场景下，MLE 容易产生极端权重，使得模型泛化能力变差。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决这些问题，我们可以像贝叶斯线性回归中一样，引入贝叶斯思想：将权重 $w$ 视为随机变量，并通过概率分布来表达我们对其的不确定性。&lt;/p&gt;
&lt;h3&gt;学习过程&lt;/h3&gt;
&lt;p&gt;在贝叶斯逻辑回归中，我们不再寻找某个 “最佳” 参数 $w$ ，而是对 &lt;strong&gt;所有可能的参数&lt;/strong&gt; 进行建模，并根据数据来更新对这些参数的 “信念” 。我们首先对参数设置一个高斯先验：&lt;/p&gt;
&lt;p&gt;$$
P(w) = \mathcal{N}(w | 0, \Sigma_0)
$$&lt;/p&gt;
&lt;p&gt;这相当于表达这样一种偏好：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;权重应当较小，并且围绕 0 分布，即模型不应过度依赖任何单一特征。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这在概率论意义上等价于逻辑回归中的 L2 正则项（具体原因可以看上一篇的深层问题思考）。&lt;/p&gt;
&lt;p&gt;对于每个训练样本 $(x_i, y_i)$ ，我们有伯努利似然：&lt;/p&gt;
&lt;p&gt;$$
P(y_i | x_i, w) = \sigma(w^{\rm T} x_i)^{y_i} \left(1 - \sigma(w^{\rm T} x_i)\right)^{1 - y_i}
$$&lt;/p&gt;
&lt;p&gt;假设数据独立，则整体似然为：&lt;/p&gt;
&lt;p&gt;$$
P(y | X, w) = \prod_{i=1}^{N} P(y_i | x_i, w)
$$&lt;/p&gt;
&lt;p&gt;这表达了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在参数为 $w$ 的情况下，观测到所有训练数据 $(X, y)$ 的概率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据贝叶斯法则，参数的后验分布为：&lt;/p&gt;
&lt;p&gt;$$
P(w | X, y) \propto P(w) , P(y | X, w)
$$&lt;/p&gt;
&lt;p&gt;这一步将 “先验信念” 与 “数据证据” 结合在一起，得到对参数 $w$ 更加合理的概率描述。&lt;/p&gt;
&lt;p&gt;不过，与贝叶斯线性回归不同，由于 logistic 似然含有 sigmoid 函数，它并不是高斯形式，因此：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后验分布不再具有解析形式，无法写成一个显式的高斯分布。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这也是贝叶斯逻辑回归以及后续高斯过程分类中需要求解的核心难点，需要借助 &lt;strong&gt;拉普拉斯近似（Laplace Approximation）&lt;/strong&gt; 或 &lt;strong&gt;变分推断（VI）&lt;/strong&gt; 等近似推断方法来求解。&lt;/p&gt;
&lt;h3&gt;预测过程&lt;/h3&gt;
&lt;p&gt;虽然贝叶斯逻辑回归的后验分布无法像线性回归那样得到解析解，但这并不妨碍我们继续理解模型如何用于预测。在深入讨论各种近似推断方法之前，我们可以先从整体流程出发，看看贝叶斯逻辑回归在已经获得参数后验分布 $ P(w∣X,y)$ 的情况下，模型究竟是如何对一个新样本 $x_*$ 做出预测。&lt;/p&gt;
&lt;p&gt;对于一个新样本 $x_*$ ，我们真正关心的是它属于正类的预测概率：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | x_*, X, y)
$$&lt;/p&gt;
&lt;p&gt;根据贝叶斯思想，这个概率应该同时考虑所有可能的参数取值，并按它们的后验概率加权。也就是说，我们不能再像普通逻辑回归那样只代入某个单一的 $w$ ，而是需要对所有 $w$ 的可能性做积分：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | x_&lt;em&gt;, X, y) = \int \sigma(w^{\rm T} x_&lt;/em&gt;) P(w | X, y) , dw
$$&lt;/p&gt;
&lt;p&gt;从形式上看，这就是对所有可能的决策边界的加权平均，因此贝叶斯逻辑回归的预测通常比传统逻辑回归更加 “保守” ，也能体现模型的不确定性。&lt;/p&gt;
&lt;p&gt;然而这个积分同样无法解析求解，原因与后验无法解析相同：Sigmoid 函数破坏了高斯结构，使得整个积分既非线性也非高斯（哪怕后验分布已经用过高斯近似）。&lt;/p&gt;
&lt;h2&gt;拉普拉斯近似&lt;/h2&gt;
&lt;p&gt;如上所述，贝叶斯逻辑回归的核心难点在于：后验分布 $P(w | X, y)$ 和预测分布 $P(y_* = 1 | x_*, X, y)$ 均无法解析求解。对此我们必须对后验分布做出某种近似。&lt;/p&gt;
&lt;p&gt;在众多近似推断方法中， &lt;strong&gt;拉普拉斯近似&lt;/strong&gt; 无疑是最直观、也最容易入门的一类。它通过对后验分布在最大后验点附近进行二阶近似，从而将一个复杂的非高斯后验替换为 “看起来像高斯” 的简单分布，既贴近直觉，又便于计算。&lt;/p&gt;
&lt;p&gt;因此在正式进入更加系统、更加通用的近似推断方法（例如后续章节中要讲到的 &lt;strong&gt;变分推断 VI&lt;/strong&gt; ）之前，我们先从拉普拉斯近似开始，理解它的核心思想与推导方式。这不仅能帮助你看懂贝叶斯逻辑回归的数学结构，也为下面理解高斯过程分类（GPC）打下概念基础。&lt;/p&gt;
&lt;h3&gt;后验分布近似&lt;/h3&gt;
&lt;p&gt;拉普拉斯近似的核心思想非常简单：在后验分布的最高点（即 MAP 点）附近，绝大多数概率质量都集中在一个局部区域。因此我们可以在这个点附近 &lt;strong&gt;用一个二阶泰勒展开来近似对数后验分布&lt;/strong&gt; ，使得后验近似成为一个高斯分布。&lt;/p&gt;
&lt;p&gt;具体来说，拉普拉斯近似包含三个关键步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;找到后验的最大值（MAP 点）&lt;/p&gt;
&lt;p&gt;也就是求解下面这个式子：&lt;/p&gt;
&lt;p&gt;$$
w_{\text{MAP}} = \arg\max_w \log P(w | X, y)
$$&lt;/p&gt;
&lt;p&gt;由于后验与先验、似然成正比，这一步相当于求解一个带 L2 正则的逻辑回归优化问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在该点附近对 log 后验进行二阶泰勒展开&lt;/p&gt;
&lt;p&gt;对该点使用泰勒展开可得：&lt;/p&gt;
&lt;p&gt;$$
\log P(w | X, y) \approx \log P(w_{\text{MAP}} | X, y) - \frac{1}{2} (w - w_{\text{MAP}})^{\rm T} H(w - w_{\text{MAP}})
$$&lt;/p&gt;
&lt;p&gt;其中 H 是负对数后验的 Hessian 矩阵：&lt;/p&gt;
&lt;p&gt;$$
H = -\nabla^2 \log P(w | X, y) \Big|&lt;em&gt;{w=w&lt;/em&gt;{\text{MAP}}}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;得到高斯形式的近似后验&lt;/p&gt;
&lt;p&gt;根据二阶展开，后验分布被近似为高斯：&lt;/p&gt;
&lt;p&gt;$$
P(w | X, y) \approx \mathcal{N}(w | w_{\text{MAP}}, H^{-1})
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此我们就用一个明确的高斯分布替代了复杂的非高斯后验，使得之前难以求解的积分与预测也变得可操作。&lt;/p&gt;
&lt;h3&gt;预测分布近似&lt;/h3&gt;
&lt;p&gt;在得到了拉普拉斯近似后的高斯后验之后，我们就可以对新样本 $x_*$ 进行预测。用拉普拉斯近似后的高斯后验代替真实后验：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | x_&lt;em&gt;, X, y) \approx \int \sigma(w^{\rm T} x_&lt;/em&gt;) \mathcal{N}(w | w_{\text{MAP}}, H^{-1}) , dw
$$&lt;/p&gt;
&lt;p&gt;令 $a = w^{\rm T} x_*$ ，则 $a$ 近似服从一维高斯分布 $a \sim \mathcal{N}(m_a, s_a^2)$ ，其参数解析式如下：&lt;/p&gt;
&lt;p&gt;$$
m_a = w_{\text{MAP}}^{\rm T} x_* \quad s_a^2 = x_&lt;em&gt;^{\rm T} H^{-1} x_&lt;/em&gt;
$$&lt;/p&gt;
&lt;p&gt;由此后验也可以写成如下形式：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | x_*, X, y) \approx \int \sigma(a) , \mathcal{N}(a | m_a, s_a^2) , da
$$&lt;/p&gt;
&lt;p&gt;这个一维积分往往没有解析解，因此我们可以通过数值方法计算，或者使用 &lt;strong&gt;常用的解析近似&lt;/strong&gt;（MacKay 公式）求解：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | x_*, X, y) \approx \sigma\left(\frac{m_a}{\sqrt{1 + \pi s_a^2 / 8}}\right)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一个经验公式，具体推导过程可以看下面这篇论文&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://authors.library.caltech.edu/records/8n76z-g9k64&quot;&gt;A practical Bayesian framework for backpropagation networks&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;理论基础&lt;/h2&gt;
&lt;p&gt;在分类问题中，我们不直接观测到连续的 $f(x)$ 值，而是观测到二元输出 $y_i \in {0, 1}$ ，表示每个数据点的类别。与回归问题不同，分类问题的目标是对这些二元输出进行建模。&lt;/p&gt;
&lt;p&gt;为了把高斯过程应用到分类问题，我们需要使用逻辑回归中的类似方式：通过某个函数（如 sigmoid 函数）将潜在函数 $f(x)$ 映射到概率空间，从而得到每个样本属于类别 1 的概率。&lt;/p&gt;
&lt;p&gt;我们假设每个样本的标签 $y_i$ 是由对应的潜在函数 $f(x_i)$ 通过一个逻辑回归的机制生成的。具体来说，给定输入 $x_i$ ，我们用 sigmoid 函数（也称为逻辑函数）来连接潜在函数值 $f_i = f(x_i)$ 和输出 $y_i$ ：&lt;/p&gt;
&lt;p&gt;$$
P(y_i = 1 | f_i) = \sigma(f_i) = \frac{1}{1 + e^{-f_i}}
$$&lt;/p&gt;
&lt;p&gt;其中 $\sigma(z)$ 是 sigmoid 函数，它将任意实数映射到 $[0, 1]$ 的概率值。&lt;/p&gt;
&lt;p&gt;因此给定潜在函数值 $f_i$ 的条件下， $y_i$ 的条件概率由以下伯努利分布描述：&lt;/p&gt;
&lt;p&gt;$$
P(y_i | f_i) = \sigma(f_i)^{y_i} \left(1 - \sigma(f_i)\right)^{1 - y_i}
$$&lt;/p&gt;
&lt;p&gt;$$
P(y | f) = \prod_{i=1}^{n} \sigma(f_i)^{y_i} \left(1 - \sigma(f_i)\right)^{1 - y_i}
$$&lt;/p&gt;
&lt;p&gt;这就为我们提供了一个关于潜在函数 $f$ 和输出 $y$ 的模型。也就是说，每个 $f_i$ 通过 sigmoid 函数生成了 $y_i$ ，因此模型的输出是基于潜在函数 $f$ 的概率。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;高斯分类代码实现&lt;/h1&gt;
&lt;p&gt;高斯过程分类的流程与高斯过程回归的流程大体相似，为了便于理解，我们将对照代码进行讲解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
from scipy.optimize import minimize
from scipy.linalg import cho_factor, cho_solve
np.random.seed(42)
X_train = np.linspace(-5, 5, 20).reshape(-1, 1)
y_train = (X_train[:, 0] &amp;gt; 0).astype(int)


# 定义 RBF 核函数
def rbf_kernel(X1, X2, length_scale=1.0, variance=1.0):
    X1 = np.atleast_2d(X1)
    X2 = np.atleast_2d(X2)
    sqdist = (
        np.sum(X1**2, 1).reshape(-1, 1)
        + np.sum(X2**2, 1)
        - 2 * X1 @ X2.T
    )
    return variance * np.exp(-0.5 / length_scale**2 * sqdist)

class GaussianProcessClassifier:
    def __init__(self, kernel, max_iter=20, tol=1e-6):
        self.kernel = kernel
        self.max_iter = max_iter
        self.tol = tol
        self.is_fit = False

    @staticmethod
    def sigmoid(a):
        return 1.0 / (1.0 + np.exp(-a))

    def fit(self, X_train, y_train):
        self.X_train = np.atleast_2d(X_train)
        self.y_train = y_train.reshape(-1, 1)
        n = len(y_train)

        def K_solve(v):
            return cho_solve(K_chol, v)

        self.K = self.kernel(self.X_train, self.X_train)
        f = np.zeros((n, 1))
        K_chol = cho_factor(self.K + 1e-6 * np.eye(n))
        for _ in range(self.max_iter):
            pi = self.sigmoid(f)
            W = (pi * (1 - pi)).flatten()

            grad = self.y_train - pi - K_solve(f)
            H = np.diag(W) + cho_solve(K_chol, np.eye(n))

            try:
                H_chol = cho_factor(H)
                delta = cho_solve(H_chol, grad)
            except:
                delta = np.linalg.solve(H, grad)

            f_new = f + delta
            if np.max(np.abs(delta)) &amp;lt; self.tol:
                f = f_new
                break
            f = f_new

        self.f_map = f
        self.W = (pi * (1 - pi)).flatten()

        H = np.diag(self.W) + cho_solve(K_chol, np.eye(n))
        H_chol = cho_factor(H)
        self.Sigma = cho_solve(H_chol, np.eye(n))

        self.K_chol = K_chol
        self.is_fit = True

    def predict(self, X_test):
        if not self.is_fit:
            raise RuntimeError(&quot;请先调用 fit() 训练模型。&quot;)

        X_test = np.atleast_2d(X_test)
        K_s = self.kernel(self.X_train, X_test)
        m_star = K_s.T @ cho_solve(self.K_chol, self.f_map)

        # predictive variance approx
        W_inv = np.diag(1.0 / (self.W + 1e-12))
        A = self.K + W_inv
        A_chol = cho_factor(A)

        v = cho_solve(A_chol, K_s)
        k_ss = self.kernel(X_test, X_test).diagonal()
        s2 = k_ss - np.sum(K_s * v, axis=0)

        # logistic integral approximation
        denom = np.sqrt(1 + np.pi * s2 / 8)
        prob = self.sigmoid(m_star.flatten() / denom)
        return prob

    def log_marginal_likelihood(self):
        f = self.f_map
        y = self.y_train
        W = self.W
        pi = self.sigmoid(f)
        log_lik = np.sum(
            y * np.log(pi + 1e-12) + (1 - y) * np.log(1 - pi + 1e-12)
        )
        K_chol = self.K_chol
        Kinv_f = cho_solve(K_chol, f)
        log_prior = -0.5 * f.T @ Kinv_f
        H = np.diag(W) + cho_solve(K_chol, np.eye(len(W)))
        H_chol = cho_factor(H)
        log_det_H = 2 * np.sum(np.log(np.diag(H_chol[0])))
        log_Z = log_lik + log_prior - 0.5 * log_det_H
        return log_Z.flatten()[0]

def optimize_rbf_hyperparameters(X_train, y_train):
    def objective(params):
        length_scale = np.exp(params[0])
        variance = np.exp(params[1])
        kernel = lambda X1, X2: rbf_kernel(
            X1, X2, length_scale=length_scale, variance=variance
        )
        gpc = GaussianProcessClassifier(kernel)
        gpc.fit(X_train, y_train)
        return -gpc.log_marginal_likelihood()

    res = minimize(
        objective,
        x0=np.log([1.0, 1.0]),
        bounds=[(-5, 5), (-5, 5)]
    )
    best_l, best_v = np.exp(res.x)
    return best_l, best_v


# 执行代码
if __name__ == &quot;__main__&quot;:
    best_l, best_v = optimize_rbf_hyperparameters(X_train, y_train)
    print(f&quot;最佳 length_scale = {best_l:.4f}, variance = {best_v:.4f}&quot;)
    kernel = lambda X1, X2: rbf_kernel(X1, X2, length_scale=best_l, variance=best_v)
    gpc = GaussianProcessClassifier(kernel)
    gpc.fit(X_train, y_train)

    X_test = np.linspace(-6, 6, 200).reshape(-1, 1)
    prob = gpc.predict(X_test)
    print(&quot;前 5 个预测概率：&quot;, prob[:5])
    print(&quot;Log Marginal Likelihood（Laplace）：&quot;, gpc.log_marginal_likelihood())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 准备过程&lt;/h2&gt;
&lt;p&gt;与高斯过程回归类似，高斯过程分类中的核函数超参数（如 RBF 核的长度尺度、幅度等）也需要通过最大化边际似然进行学习。&lt;/p&gt;
&lt;p&gt;在回归任务中，由于观测噪声服从高斯分布，模型始终保持高斯结构，因此可以直接写出对数边际似然的解析形式：&lt;/p&gt;
&lt;p&gt;$$
\log P(y | X, \theta) = -\frac{1}{2} y^{\rm T} (K + \sigma_n^2 \mathbf{I})^{-1} y - \frac{1}{2} \log |K + \sigma_n^2 \mathbf{I}| - \frac{N}{2} \log 2\pi
$$&lt;/p&gt;
&lt;p&gt;这使得高斯过程回归的训练过程在数学上非常简洁，无需额外的近似方法。&lt;/p&gt;
&lt;p&gt;然而在分类问题中情况完全不同。由于输出 $y_i \in {0,1}$ ，似然由伯努利分布决定，并通过 sigmoid 链接函数与潜在函数 $f$ 关联，因此整个模型不再保持高斯形式。结果是后验分布无法写成高斯，边际似然自然也无法解析地求解。&lt;/p&gt;
&lt;p&gt;为了继续进行参数学习，我们必须对后验分布进行近似，而最常用、最直接的方法便是拉普拉斯近似。它与我们在贝叶斯逻辑回归中使用的方法完全一致（具体求解过程可以参考&lt;a href=&quot;#%E6%8B%89%E6%99%AE%E6%8B%89%E6%96%AF%E8%BF%91%E4%BC%BC&quot;&gt;上面&lt;/a&gt;的拉普拉斯近似讲解，下面的学习过程也给出具体流程，这里假设该分布已经求出）：&lt;/p&gt;
&lt;p&gt;$$
P(f | X, y, \theta) \approx \mathcal{N}(f_{\text{MAP}}, H^{-1})
$$&lt;/p&gt;
&lt;p&gt;接下来利用贝叶斯恒等式：&lt;/p&gt;
&lt;p&gt;$$
P(f | X, y, \theta) = \frac{P(y | f, X) P(f | X, \theta)}{P(y | X, \theta)}
$$&lt;/p&gt;
&lt;p&gt;在 $f = f_\text{MAP}$ 处取其对数可得：&lt;/p&gt;
&lt;p&gt;$$
\log P(y | X, \theta) = \log P(y | f_{\text{MAP}}, X) + \log P(f_{\text{MAP}} | X, \theta) - \log P(f_{\text{MAP}} | X, y, \theta)
$$&lt;/p&gt;
&lt;p&gt;其中第一项是似然项，直接套用对数伯努利似然公式即可：&lt;/p&gt;
&lt;p&gt;$$
\log P(y | f_{\text{MAP}}, X) = \sum_i \Big[ y_i \log \sigma(f_{\text{MAP},i}) + (1 - y_i) \log \big(1 - \sigma(f_{\text{MAP},i})\big) \Big]
$$&lt;/p&gt;
&lt;p&gt;第二项是先验项，我们依旧假设先验服从高斯分布 $f \sim \mathcal{N}(0, K_{\theta})$（本质就是一个高斯过程），取对数后可得：&lt;/p&gt;
&lt;p&gt;$$
\log P(f_{\text{MAP}} \mid X, \theta) = -\frac{1}{2} f_{\text{MAP}}^{\rm T} K_\theta^{-1} f_{\text{MAP}} - \frac{1}{2} \log |K_\theta| - \frac{n}{2} \log 2\pi
$$&lt;/p&gt;
&lt;p&gt;第三项是后验项，由前面的拉普拉斯近似求出：&lt;/p&gt;
&lt;p&gt;$$
-\log P(f_{\text{MAP}} | X, y, \theta) \approx \frac{1}{2} \log |H| + \frac{n}{2} \log 2\pi
$$&lt;/p&gt;
&lt;p&gt;将上述三项整理，可以得到高斯过程分类中常用的对数边际似然近似：&lt;/p&gt;
&lt;p&gt;$$
\log P(y | X, \theta) \approx \ell(f_{\text{MAP}}) - \frac{1}{2} f_{\text{MAP}}^{\rm T} K_\theta^{-1} f_{\text{MAP}} - \frac{1}{2} \log |H| + \frac{n}{2} \log 2\pi
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 定义 RBF 核函数
def rbf_kernel(X1, X2, length_scale=1.0, variance=1.0):
    X1 = np.atleast_2d(X1)
    X2 = np.atleast_2d(X2)
    sqdist = (
        np.sum(X1**2, 1).reshape(-1, 1)
        + np.sum(X2**2, 1)
        - 2 * X1 @ X2.T
    )
    return variance * np.exp(-0.5 / length_scale**2 * sqdist)

# 对数边际似然函数
def log_marginal_likelihood(self):
    f = self.f_map
    y = self.y_train
    W = self.W
    pi = self.sigmoid(f)
    log_lik = np.sum(
        y * np.log(pi + 1e-12) + (1 - y) * np.log(1 - pi + 1e-12)
    )
    K_chol = self.K_chol
    Kinv_f = cho_solve(K_chol, f)
    log_prior = -0.5 * f.T @ Kinv_f
    H = np.diag(W) + cho_solve(K_chol, np.eye(len(W)))
    H_chol = cho_factor(H)
    log_det_H = 2 * np.sum(np.log(np.diag(H_chol[0])))
    log_Z = log_lik + log_prior - 0.5 * log_det_H
    return log_Z.flatten()[0]

# 超参数优化函数
def optimize_rbf_hyperparameters(X_train, y_train):
    def objective(params):
        length_scale = np.exp(params[0])
        variance = np.exp(params[1])
        kernel = lambda X1, X2: rbf_kernel(
            X1, X2, length_scale=length_scale, variance=variance
        )
        gpc = GaussianProcessClassifier(kernel)
        gpc.fit(X_train, y_train)
        return -gpc.log_marginal_likelihood()

    res = minimize(
        objective,
        x0=np.log([1.0, 1.0]),
        bounds=[(-5, 5), (-5, 5)]
    )
    best_l, best_v = np.exp(res.x)
    return best_l, best_v
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 学习过程&lt;/h2&gt;
&lt;p&gt;在高斯过程分类中，训练目标是学习潜在函数 $f$ 的分布，使得它能够解释训练标签 $y$ 的观测情况。由于伯努利似然与高斯先验不兼容，后验 $P(f|X, y, \theta)$ 不再是高斯，无法直接解析求解。因此学习过程的核心是对后验进行近似，我们依旧使用拉普拉斯近似：&lt;/p&gt;
&lt;p&gt;$$
P(f | X, y, \theta) \approx \mathcal{N}(f_{\text{MAP}}, H^{-1})
$$&lt;/p&gt;
&lt;p&gt;我们来求解这个高斯分布的参数，首先构造对数后验：&lt;/p&gt;
&lt;p&gt;$$
\log P(f | X, y, \theta) = \log P(y | f) + \log P(f | X, \theta)
$$&lt;/p&gt;
&lt;p&gt;然后对 $f$ 求导并令其为零可得：&lt;/p&gt;
&lt;p&gt;$$
\nabla_f \log P(f | X, y, \theta) = 0
$$&lt;/p&gt;
&lt;p&gt;这通常通过 &lt;strong&gt;牛顿法&lt;/strong&gt; 求解，因为 $\log P(y|f)$ 的形式是非线性的。&lt;/p&gt;
&lt;p&gt;只要得到了 $f_\text{MAP}$ 就可以使用公式直接求解出 Hessian 矩阵：&lt;/p&gt;
&lt;p&gt;$$
H = -\nabla_f^2 \log P(f | X, y, \theta) \Big|&lt;em&gt;{f = f&lt;/em&gt;{\text{MAP}}}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fit(self, X_train, y_train):
    self.X_train = np.atleast_2d(X_train)
    self.y_train = y_train.reshape(-1, 1)
    n = len(y_train)

    def K_solve(v):
        return cho_solve(K_chol, v)

    self.K = self.kernel(self.X_train, self.X_train)
    f = np.zeros((n, 1))
    K_chol = cho_factor(self.K + 1e-6 * np.eye(n))
    for _ in range(self.max_iter):
        pi = self.sigmoid(f)
        W = (pi * (1 - pi)).flatten()

        grad = self.y_train - pi - K_solve(f)
        H = np.diag(W) + cho_solve(K_chol, np.eye(n))

        try:
            H_chol = cho_factor(H)
            delta = cho_solve(H_chol, grad)
        except:
            delta = np.linalg.solve(H, grad)

        f_new = f + delta
        if np.max(np.abs(delta)) &amp;lt; self.tol:
            f = f_new
            break
        f = f_new

    self.f_map = f
    self.W = (pi * (1 - pi)).flatten()

    H = np.diag(self.W) + cho_solve(K_chol, np.eye(n))
    H_chol = cho_factor(H)
    self.Sigma = cho_solve(H_chol, np.eye(n))

    self.K_chol = K_chol
    self.is_fit = True
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 预测过程&lt;/h2&gt;
&lt;p&gt;假设我们在学习阶段得到了后验的拉普拉斯近似，现在要对新的输入点 $X_&lt;em&gt;$ 预测潜在函数值 $f_&lt;/em&gt;$ 以及标签 $y_*$ ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;潜在函数预测&lt;/p&gt;
&lt;p&gt;潜在函数 $f_*$ 的条件分布为：&lt;/p&gt;
&lt;p&gt;$$
f_* | X_&lt;em&gt;, X, f_{\text{MAP}}, \theta \sim \mathcal{N}(\mu_&lt;/em&gt;, \sigma_*^2)
$$&lt;/p&gt;
&lt;p&gt;其中的参数解析式为（直接套用高维高斯分布的参数公式）：&lt;/p&gt;
&lt;p&gt;$$
\mu_* = K(X_*, X) K^{-1} f_{\text{MAP}}
$$&lt;/p&gt;
&lt;p&gt;$$
\sigma_&lt;em&gt;^2 = K(X_&lt;/em&gt;, X_&lt;em&gt;) - K(X_&lt;/em&gt;, X) (K + W^{-1})^{-1} K(X, X_*)
$$&lt;/p&gt;
&lt;p&gt;其中 $W$ 是 $H$ 的一部分，满足公式 $H = K^{-1} + W$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标签预测&lt;/p&gt;
&lt;p&gt;由于输出是二分类，我们需要将潜在函数 $f_*$ 经过 Sigmoid 映射得到类别概率：&lt;/p&gt;
&lt;p&gt;$$
P(y_* = 1 | X_&lt;em&gt;, X, y) = \int \sigma(f_&lt;/em&gt;) P(f_* | X_&lt;em&gt;, X, y) , df_&lt;/em&gt;
$$&lt;/p&gt;
&lt;p&gt;这个积分依旧是没有解析解，因此常用近似方法，这里不再给出。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为分类标签无法直接求解并且只与潜在函数有关，因此我们要先求解潜在函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def predict(self, X_test):
    if not self.is_fit:
        raise RuntimeError(&quot;请先调用 fit() 训练模型。&quot;)

    X_test = np.atleast_2d(X_test)
    K_s = self.kernel(self.X_train, X_test)
    m_star = K_s.T @ cho_solve(self.K_chol, self.f_map)

    # predictive variance approx
    W_inv = np.diag(1.0 / (self.W + 1e-12))
    A = self.K + W_inv
    A_chol = cho_factor(A)

    v = cho_solve(A_chol, K_s)
    k_ss = self.kernel(X_test, X_test).diagonal()
    s2 = k_ss - np.sum(K_s * v, axis=0)

    # logistic integral approximation
    denom = np.sqrt(1 + np.pi * s2 / 8)
    prob = self.sigmoid(m_star.flatten() / denom)
    return prob
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;贝叶斯逻辑回归&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@wtc2189/day-6-a-short-intro-to-bayesian-logistic-regression-6141c1d1d6c9&quot;&gt;A Short Intro to Bayesian Logistic Regression&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.inference.vc/variational-inference-with-implicit-probabilistic-models-part-1-2/&quot;&gt;Variational Inference using Implicit Models&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://darrenjw.wordpress.com/2022/08/07/bayesian-inference-for-a-logistic-regression-model-part-1/&quot;&gt;Bayesian inference for a logistic regression model&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高斯过程分类&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://apxml.com/zh/courses/bayesian-machine-learning/chapter-4-gaussian-processes/gp-classification-techniques&quot;&gt;高斯过程分类方法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/1.6/modules/generated/sklearn.gaussian_process.GaussianProcessClassifier.html&quot;&gt;GaussianProcessClassifier（高斯过程分类器）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pdf.hanspub.org/AIRR20220400000_64886947.pdf&quot;&gt;基于高斯过程分类的小样本图像识别&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://nlpr.ia.ac.cn/2009papers/gnkw/nk9.pdf&quot;&gt;结合半监督核的高斯过程分类&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第十节：高斯过程回归</title><link>https://xingguang641.com/posts/regression-model/gaussian-process-regression/</link><guid isPermaLink="true">https://xingguang641.com/posts/regression-model/gaussian-process-regression/</guid><description>介绍机器学习常见的模型</description><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;高斯回归基本原理&lt;/h1&gt;
&lt;p&gt;在传统的机器学习中， &lt;strong&gt;线性回归&lt;/strong&gt; 是最基础的监督学习模型之一。它通过一条直线（或超平面）去拟合数据的整体趋势，从而建立输入与输出之间的关系。然而在很多复杂任务中，现实世界的数据往往呈现出 &lt;strong&gt;非线性、不确定性强&lt;/strong&gt; 的特征，此时单纯的线性假设已无法很好地刻画真实关系。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，人们引入了 &lt;strong&gt;概率建模&lt;/strong&gt; 的思想 ———— 我们不仅希望得到一个预测值，还希望能够衡量模型预测的不确定性，即给出预测结果的 &lt;strong&gt;置信度&lt;/strong&gt; 。在这一思路下， &lt;strong&gt;高斯过程回归&lt;/strong&gt;（Gaussian Process Regression，简称 GPR） 便应运而生。它是一种以概率论为基础的非参数方法，能够在建模复杂函数的同时，对预测结果的不确定性进行定量刻画。&lt;/p&gt;
&lt;p&gt;然而我们要真正理解高斯过程回归的思想，首先需要从它的简化形式出发，也就是 &lt;strong&gt;贝叶斯线性回归（Bayesian Linear Regression）&lt;/strong&gt;。高斯过程回归正是基于贝叶斯回归思想的一种推广与扩展。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cregression-model%5C%E9%AB%98%E6%96%AF%E8%BF%87%E7%A8%8B%E5%9B%9E%E5%BD%921.jpg&quot; alt=&quot;高斯过程回归图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;贝叶斯线性回归&lt;/h2&gt;
&lt;p&gt;在线性回归中，我们假设输入特征 $x$ 与输出 $y$ 之间满足线性关系：&lt;/p&gt;
&lt;p&gt;$$
y = w^{\rm T} x + \epsilon
$$&lt;/p&gt;
&lt;p&gt;其中 $\epsilon \sim \mathcal{N}(0, \beta^{-1})$ 表示高斯噪声。&lt;/p&gt;
&lt;p&gt;传统的最小二乘回归（OLS）会通过最小化均方误差（MSE）来估计参数：&lt;/p&gt;
&lt;p&gt;$$
\hat{w} = \arg \min_w \sum_i (y_i - w^{\rm T} x_i)^2
$$&lt;/p&gt;
&lt;p&gt;这种方法得到的是参数 $w$ 的 &lt;strong&gt;点估计（point estimate）&lt;/strong&gt;，这是一个确定的结果。然而在现实中，训练数据往往 &lt;strong&gt;有限、带噪声甚至存在异常点&lt;/strong&gt; ，因此我们在对参数估计时应该要参杂一点 “不确定性” 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面的教程舍去了不必要的证明过程，如果感兴趣的读者可以观看这篇博客&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/305042203&quot;&gt;浅述贝叶斯线性回归&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;贝叶斯思想&lt;/h3&gt;
&lt;p&gt;在贝叶斯线性回归中，我们引入一个重要的思想：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们不把参数 $w$ 看作确定值，而是把它当作一个 &lt;strong&gt;随机变量&lt;/strong&gt; ，用概率分布来描述我们对它的信念。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，我们要建模的是 $P(w|\mathcal{D})$ ：即给定数据集 $\mathcal{D} = { (x_i, y_i) }_{i=1}^N$ 的条件下，参数 $w$ 的 &lt;strong&gt;后验分布&lt;/strong&gt; 。这个分布告诉我们：在观察到数据之后，哪些 $w$ 是更可能的，哪些是不太可能的。这样我们不仅能够得到预测结果，还能量化预测的 “置信度” 。&lt;/p&gt;
&lt;p&gt;为了得到这个后验分布，我们需要先引入两个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先验分布（Prior）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在未观察到任何数据之前，我们对参数 $w$ 的可能取值有一个先验假设。最常见的假设是各维度相互独立、均值为 0 的高斯分布：&lt;/p&gt;
&lt;p&gt;$$
P(w) = \mathcal{N}(w | 0, \alpha^{-1} \mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;其中 $\alpha$ 表示先验分布的精度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;似然函数（Likelihood）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设观测数据由参数 $w$ 生成，并受到高斯噪声的影响：&lt;/p&gt;
&lt;p&gt;$$
P(y | x, w) = \mathcal{N}(y | w^{\rm T} x, \beta^{-1})
$$&lt;/p&gt;
&lt;p&gt;对于整个数据集 $\mathcal{D} = {(x_i, y_i)}_{i=1}^N$ ，若假设样本独立同分布，则整体似然为：&lt;/p&gt;
&lt;p&gt;$$
P(y | X, w) = \prod_{i=1}^{N} \mathcal{N}(y_i | w^{\rm T} \mathbf{x}_i, \beta^{-1})
$$&lt;/p&gt;
&lt;p&gt;其中 $X$ 是特征矩阵， $y$ 是观测输出， $\beta$ 表示观测噪声的精度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;学习过程&lt;/h3&gt;
&lt;p&gt;正如上面所说：在贝叶斯线性回归中，我们的目标不再是寻找一个最优参数 $\hat{w}$ ，而是希望通过数据推断出参数的后验分布：&lt;/p&gt;
&lt;p&gt;$$
P(w | X, y) = \frac{P(y | X, w) P(w)}{P(y | X)}
$$&lt;/p&gt;
&lt;p&gt;由于我们假设先验与似然均为高斯分布，它们的乘积仍为高斯分布，因此后验分布 $P(w | X, y)$ 也服从高斯形式：&lt;/p&gt;
&lt;p&gt;$$
P(w | X, y) = \mathcal{N}(w | \mathbf{m}_N, \mathbf{S}_N)
$$&lt;/p&gt;
&lt;p&gt;将先验分布和似然函数带入贝叶斯公式后，可以得到后验分布的参数解析式：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{m}_N = \beta \mathbf{S}_N X^{\rm T} y \quad \mathbf{S}_N = (\alpha \mathbf{I} + \beta X^{\rm T} X)^{-1}
$$&lt;/p&gt;
&lt;h3&gt;预测过程&lt;/h3&gt;
&lt;p&gt;在获得参数的后验分布 $P(w | X, y)$ 后，我们就可以对新样本 $x_*$ 进行预测。与传统线性回归直接使用单一参数 $\hat{w}$ 不同，贝叶斯线性回归会综合考虑所有可能的参数值，并按照其后验概率加权平均：&lt;/p&gt;
&lt;p&gt;$$
P(y_* | x_&lt;em&gt;, X, y) = \int P(y_&lt;/em&gt; | x_*, w) , P(w | X, y) , dw
$$&lt;/p&gt;
&lt;p&gt;这一积分表达了核心的贝叶斯思想：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;预测不仅依赖模型的结构，还依赖我们对参数不确定性的认知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于被积函数的两项（条件分布和后验分布）均为高斯形式，积分结果仍为高斯分布：&lt;/p&gt;
&lt;p&gt;$$
P(y_* | x_&lt;em&gt;, X, y) = \mathcal{N}(y_&lt;/em&gt; | \mu_&lt;em&gt;, \sigma_&lt;/em&gt;^2)
$$&lt;/p&gt;
&lt;p&gt;将后验分布（我们学习得到的模型）和似然函数带入积分后，可以得到预测分布的参数解析式：&lt;/p&gt;
&lt;p&gt;$$
\mu_* = \mathbf{m}&lt;em&gt;N^{\rm T} x&lt;/em&gt;* \quad \sigma_&lt;em&gt;^2 = \frac{1}{\beta} + x_&lt;/em&gt;^{\rm T} \mathbf{S}&lt;em&gt;N x&lt;/em&gt;*
$$&lt;/p&gt;
&lt;h2&gt;高斯过程&lt;/h2&gt;
&lt;p&gt;通过贝叶斯线性回归，我们学会了如何在参数层面引入不确定性：我们不再寻找单一的参数向量 $w$ ，而是对其建立一个概率分布 $P(w|X, y)$ 。这使得模型能够在预测时量化不确定性，从而变得更稳健、更可信。然而线性模型本身仍然受到 &lt;strong&gt;特征空间的限制&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在贝叶斯线性回归中，即使我们对参数 $w$ 进行了概率建模，模型仍然只能表达输入 $x$ 的线性组合形式。&lt;/p&gt;
&lt;p&gt;为了表达非线性关系，我们可以通过引入 &lt;strong&gt;特征映射函数&lt;/strong&gt; $\phi(x)$ ，将输入投影到更高维的空间：&lt;/p&gt;
&lt;p&gt;$$
f(x) = w^{\rm T} \phi(x)
$$&lt;/p&gt;
&lt;p&gt;但这样一来，模型的复杂度和参数数量会急剧增加，计算和存储都变得困难。&lt;/p&gt;
&lt;p&gt;于是我们换一种视角：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;与其对参数 $w$ 进行建模，不如直接对函数 $f(x)$ 本身建模。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，我们不再假设存在某个确定的 $w$ ，而是假设所有可能的函数 $f(x)$ 构成了一个分布。而 &lt;strong&gt;高斯过程&lt;/strong&gt;（Gaussian Process，简称 GP）就是这样一种 “对函数分布建模” 的方法。&lt;/p&gt;
&lt;p&gt;如果我们从函数上采样若干个点，然后将每个点都看成一个高斯分布（给每个点都加上随机扰动），那么我们就可以写出这些点的联合高斯分布来表示整个函数。形式化地，我们假设：&lt;/p&gt;
&lt;p&gt;$$
f(x) \sim \mathcal{GP}(m(x), k(x, x&apos;))
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\displaystyle m(x) = \mathbb{E}\Big[f(x)\Big]$ 为 &lt;strong&gt;均值函数&lt;/strong&gt; ，描述函数在输入空间的平均趋势&lt;/li&gt;
&lt;li&gt;$\displaystyle k(x, x&apos;) = \mathbb{E}\Big[\big(f(x) - m(x)\big)\big(f(x&apos;) - m(x&apos;)\big)\Big]$ 为 &lt;strong&gt;核函数&lt;/strong&gt; ，刻画任意两个输入点之间的相关性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了理解高斯过程的由来，我们可以从贝叶斯线性回归出发。回忆贝叶斯线性回归中的预测函数：&lt;/p&gt;
&lt;p&gt;$$
f(x) = w^{\rm T} \phi(x)
$$&lt;/p&gt;
&lt;p&gt;假设参数具有高斯先验 $P(w) = \mathcal{N}(0, \alpha^{-1} \mathbf{I})$ 那么我们可以计算出函数值的期望与协方差：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}\Big[f(x)\Big] = 0 \quad \text{Cov}\Big(f(x), f(x&apos;)\Big) = \alpha^{-1} \phi(x)^{\rm T} \phi(x&apos;)
$$&lt;/p&gt;
&lt;p&gt;说明在这种假设下 $f(x)$ 本身服从一个高斯过程，其协方差函数为：&lt;/p&gt;
&lt;p&gt;$$
k(x, x&apos;) = \alpha^{-1} \phi(x)^{\rm T} \phi(x&apos;)
$$&lt;/p&gt;
&lt;p&gt;也就是说，贝叶斯线性回归天然地隐含了一个高斯过程假设。如果我们令特征映射 $\phi(x)$ 的维度趋于无穷，并用核函数直接定义 $k(x, x&apos;)$ ，就得到了 &lt;strong&gt;高斯过程回归&lt;/strong&gt; ———— 贝叶斯线性回归的无限维形式。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;高斯回归代码实现&lt;/h1&gt;
&lt;p&gt;在前面的讨论中我们已经知道：高斯过程是一种直接对函数建立分布的建模方式。它不再依赖具体的参数向量 $w$ ，而是通过核函数 $k(x, x&apos;)$ 来刻画不同输入点之间的相关性。基于这种思想，我们现在来正式推导高斯过程回归的完整过程。&lt;/p&gt;
&lt;p&gt;为了便于理解，我们将对照代码进行讲解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
from scipy.optimize import minimize
np.random.seed(42)
X_train = np.linspace(-5, 5, 12).reshape(-1, 1)
y_train = np.sin(X_train) + 0.3 * np.random.randn(*X_train.shape)


# 定义 RBF 核函数
def rbf_kernel(X1, X2, length_scale=1.0, variance=1.0):
    X1 = np.atleast_2d(X1); X2 = np.atleast_2d(X2)
    sqdist = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return variance * np.exp(-0.5 / (length_scale**2) * sqdist)

class GaussianProcessRegressor:
    def __init__(self, kernel, noise=1e-6):
        self.kernel = kernel
        self.noise = noise
        self.is_fit = False

    def fit(self, X_train, y_train):
        self.X_train = np.atleast_2d(X_train)
        self.y_train = np.atleast_2d(y_train).reshape(-1, 1)
        K = self.kernel(self.X_train, self.X_train)
        K_y = K + self.noise * np.eye(len(self.X_train))
        self.L = np.linalg.cholesky(K_y)
        self.alpha = np.linalg.solve(self.L.T, np.linalg.solve(self.L, self.y_train))
        self.is_fit = True

    def predict(self, X_test, return_cov=False):
        if not self.is_fit:
            raise RuntimeError(&quot;模型尚未训练，请先调用 fit()。&quot;)
        X_test = np.atleast_2d(X_test)
        K_s = self.kernel(self.X_train, X_test)
        K_ss = self.kernel(X_test, X_test)
        mu = K_s.T @ self.alpha
        v = np.linalg.solve(self.L, K_s)
        cov = K_ss - v.T @ v
        if return_cov:
            return mu.ravel(), cov
        else:
            return mu.ravel()

    def log_marginal_likelihood(self):
        y = self.y_train
        L = self.L
        n = len(y)
        term1 = -0.5 * y.T @ self.alpha
        term2 = -np.sum(np.log(np.diag(L)))
        term3 = -0.5 * n * np.log(2 * np.pi)
        return (term1 + term2 + term3).ravel()[0]

# 超参数优化函数
def optimize_rbf_hyperparameters(X_train, y_train, noise=0.1**2):
    def objective(params):
        # 对数变换保证参数 &amp;gt;0
        length_scale = np.exp(params[0])
        variance = np.exp(params[1])
        kernel = lambda X1, X2: rbf_kernel(X1, X2, length_scale=length_scale, variance=variance)
        gp = GaussianProcessRegressor(kernel=kernel, noise=noise)
        gp.fit(X_train, y_train)
        return -gp.log_marginal_likelihood()

    res = minimize(objective, x0=np.log([1.0, 1.0]), bounds=[(-5, 5), (-5, 5)])
    best_length_scale, best_variance = np.exp(res.x)
    return best_length_scale, best_variance

# 执行代码
noise = 0.1**2
if __name__ == &quot;__main__&quot;:
    # 优化所有核参数
    best_l, best_v = optimize_rbf_hyperparameters(X_train, y_train, noise=noise)
    print(f&quot;优化得到的 length_scale: {best_l:.4f}, variance: {best_v:.4f}&quot;)
    # 使用优化后的核参数重新训练模型
    gp = GaussianProcessRegressor(
        kernel=lambda x, y: rbf_kernel(x, y, length_scale=best_l, variance=best_v),
        noise=noise
    )
    gp.fit(X_train, y_train)

    # 预测过程
    X_test = np.linspace(-6, 6, 200).reshape(-1, 1)
    mean, cov = gp.predict(X_test, return_cov=True)
    std = np.sqrt(np.diag(cov))
    print(&quot;Posterior mean (前5个):&quot;, mean[:5])
    print(&quot;Posterior std  (前5个):&quot;, std[:5])
    print(&quot;Log Marginal Likelihood:&quot;, gp.log_marginal_likelihood())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 准备过程&lt;/h2&gt;
&lt;p&gt;高斯过程回归依赖 &lt;strong&gt;核函数&lt;/strong&gt; 来刻画输入点之间的相关性。核函数的参数通常可以由人工设定，但也可以通过 &lt;strong&gt;最大化边际似然（Max Marginal Likelihood）&lt;/strong&gt; 自动学习得到。&lt;/p&gt;
&lt;p&gt;假设我们有一组训练数据 $\mathcal{D} = {(x_i, y_i)}_{i=1}^N$ ，其中观测值 $y_i$ 由真实函数 $f(x_i)$ 加上噪声生成：&lt;/p&gt;
&lt;p&gt;$$
y_i = f(x_i) + \epsilon_i \quad \epsilon_i \sim \mathcal{N}(0, \sigma_n^2)
$$&lt;/p&gt;
&lt;p&gt;我们进一步假设函数 $f(x)$ 服从一个高斯过程（为简化推导，通常假设均值函数为零）：&lt;/p&gt;
&lt;p&gt;$$
f(x) \sim \mathcal{GP}(0, k(x, x&apos;))
$$&lt;/p&gt;
&lt;p&gt;于是对任意有限个输入点 ${x_1, x_2, \dots, x_N}$ ，其函数值向量服从多元高斯分布：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{f} = [f(x_1), f(x_2), \ldots, f(x_N)]^{\rm T} \sim \mathcal{N}(0, K)
$$&lt;/p&gt;
&lt;p&gt;其中协方差矩阵 $K$ 的元素由核函数 $K_{ij} = k(x_i, x_j)$ 给出。&lt;/p&gt;
&lt;p&gt;由于观测值 $y$ 受到噪声影响，其分布为：&lt;/p&gt;
&lt;p&gt;$$
y \sim \mathcal{N}(0, K + \sigma_n^2 \mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;我们就可以直接写出边际似然 $P(y|X, \theta)$（关于这个分布为什么叫边际似然可以看下面的&lt;a href=&quot;#%E6%B7%B1%E5%B1%82%E9%97%AE%E9%A2%98%E6%80%9D%E8%80%83&quot;&gt;深层问题思考&lt;/a&gt;）：&lt;/p&gt;
&lt;p&gt;$$
P(y|X, \theta) = \mathcal{N}(0, K + \sigma_n^2 \mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;其对数形式为：&lt;/p&gt;
&lt;p&gt;$$
\log P(y | X, \theta) = -\frac{1}{2} y^{\rm T} (K + \sigma_n^2 \mathbf{I})^{-1} y - \frac{1}{2} \log |K + \sigma_n^2 \mathbf{I}| - \frac{N}{2} \log 2\pi
$$&lt;/p&gt;
&lt;p&gt;通过最大化这个对数边际似然，我们可以找到最优的核函数超参数 $\theta$ 。在实际计算中，常用的方法包括梯度下降或 L-BFGS 等数值优化算法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 定义 RBF 核函数
def rbf_kernel(X1, X2, length_scale=1.0, variance=1.0):
    X1 = np.atleast_2d(X1); X2 = np.atleast_2d(X2)
    sqdist = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return variance * np.exp(-0.5 / (length_scale**2) * sqdist)

# 对数边际似然函数
def log_marginal_likelihood(self):
    y = self.y_train
    L = self.L
    n = len(y)
    term1 = -0.5 * y.T @ self.alpha
    term2 = -np.sum(np.log(np.diag(L)))
    term3 = -0.5 * n * np.log(2 * np.pi)
    return (term1 + term2 + term3).ravel()[0]

# 超参数优化函数
def optimize_rbf_hyperparameters(X_train, y_train, noise=0.1**2):
    def objective(params):
        # 对数变换保证参数 &amp;gt;0
        length_scale = np.exp(params[0])
        variance = np.exp(params[1])
        kernel = lambda X1, X2: rbf_kernel(X1, X2, length_scale=length_scale, variance=variance)
        gp = GaussianProcessRegressor(kernel=kernel, noise=noise)
        gp.fit(X_train, y_train)
        return -gp.log_marginal_likelihood()

    res = minimize(objective, x0=np.log([1.0, 1.0]), bounds=[(-5, 5), (-5, 5)])
    best_length_scale, best_variance = np.exp(res.x)
    return best_length_scale, best_variance
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 学习过程&lt;/h2&gt;
&lt;p&gt;在准备阶段中我们已经推导出观测向量满足：&lt;/p&gt;
&lt;p&gt;$$
y \sim \mathcal{N}(0, K + \sigma_n^2 \mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;但在实际的代码实现中，我们并不直接显式求解该分布的均值和协方差，而是希望得到预测过程中所需要的关键参数 $\alpha$ ：&lt;/p&gt;
&lt;p&gt;$$
\alpha = (K + \sigma_n^2 \mathbf{I})^{-1} y
$$&lt;/p&gt;
&lt;p&gt;这样在预测新的输入时，均值可以简化为（这样可以反复消除求逆带来的开销）：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}\Big[f_&lt;em&gt;\Big] = K(X_&lt;/em&gt;, X_{\text{train}}) (K + \sigma_n^2 \mathbf{I})^{-1} y = K(X_*, X_{\text{train}}) \alpha
$$&lt;/p&gt;
&lt;p&gt;由于对矩阵直接求逆不仅计算代价高、数值稳定性较差，因此通常采用 Cholesky 分解：&lt;/p&gt;
&lt;p&gt;$$
K + \sigma_n^2 \mathbf{I} = LL^{\rm T}
$$&lt;/p&gt;
&lt;p&gt;其中 $L$ 为下三角矩阵，可以通过两次三角求解就可以得到 $\alpha$ ：&lt;/p&gt;
&lt;p&gt;$$
Lz = y  \quad \Rightarrow \quad L^{\rm T} \alpha = z
$$&lt;/p&gt;
&lt;p&gt;需要注意的是，这些用于预测的关键参数（包括 $\alpha$ 和 $L$ ）都依赖于核矩阵：&lt;/p&gt;
&lt;p&gt;$$
K_\theta = K(X_{\text{train}}, X_{\text{train}}; \theta)
$$&lt;/p&gt;
&lt;p&gt;该矩阵已在上述的准备过程中学习得到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fit(self, X_train, y_train):
    self.X_train = np.atleast_2d(X_train)
    self.y_train = np.atleast_2d(y_train).reshape(-1, 1)
    K = self.kernel(self.X_train, self.X_train)
    K_y = K + self.noise * np.eye(len(self.X_train))
    # Cholesky 分解提高数值稳定性
    self.L = np.linalg.cholesky(K_y)
    self.alpha = np.linalg.solve(self.L.T, np.linalg.solve(self.L, self.y_train))
    self.is_fit = True
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 预测过程&lt;/h2&gt;
&lt;p&gt;当模型完成训练后，我们希望对新的输入点 $X_*$ 给出预测。&lt;/p&gt;
&lt;p&gt;在高斯过程回归中，预测结果本质上来自 &lt;strong&gt;多元高斯分布的条件分布公式&lt;/strong&gt; ，因此具有严格的解析解：&lt;/p&gt;
&lt;p&gt;$$
f_* | X_&lt;em&gt;, X_{\text{train}}, y \sim \mathcal{N}(\mu_&lt;/em&gt;, \Sigma_*)
$$&lt;/p&gt;
&lt;p&gt;其中的均值与协方差的解析式为（直接套用高维高斯分布的参数公式）：&lt;/p&gt;
&lt;p&gt;$$
\mu_* = K(X_*, X_{\text{train}}) (K + \sigma_n^2 \mathbf{I})^{-1} y
$$&lt;/p&gt;
&lt;p&gt;$$
\Sigma_* = K(X_&lt;em&gt;, X_&lt;/em&gt;) - K(X_{\text{train}}, X_&lt;em&gt;)^{\rm T} (K + \sigma_n^2 I)^{-1} K(X_{\text{train}}, X_&lt;/em&gt;)
$$&lt;/p&gt;
&lt;p&gt;将预测参数 $\alpha = (K + \sigma_n^2 \mathbf{I})^{-1} y$ 代入公式可将均值化简为：&lt;/p&gt;
&lt;p&gt;$$
\mu_* = K(X_*, X_{\text{train}}) \alpha
$$&lt;/p&gt;
&lt;p&gt;将辅助矩阵 $v = L^{-1} K(X_{\text{train}}, X_*)$ 代入公式可将协方差化简为：&lt;/p&gt;
&lt;p&gt;$$
\Sigma_* = K(X_&lt;em&gt;, X_&lt;/em&gt;) - v^{\rm T} v
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def predict(self, X_test, return_cov=False):
    if not self.is_fit:
        raise RuntimeError(&quot;模型尚未训练，请先调用 fit()。&quot;)
    X_test = np.atleast_2d(X_test)
    K_s = self.kernel(self.X_train, X_test)
    K_ss = self.kernel(X_test, X_test)
    mu = K_s.T @ self.alpha
    v = np.linalg.solve(self.L, K_s)
    cov = K_ss - v.T @ v
    if return_cov:
        return mu.ravel(), cov
    else:
        return mu.ravel()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;贝叶斯思想中的先验分布和似然函数为什么都是高斯分布？（为什么贝叶斯线性回归优于普通线性回归？）&lt;/p&gt;
&lt;p&gt;我们先来讲解一下为什么似然函数要设成：&lt;/p&gt;
&lt;p&gt;$$
P(y | x, w) = \mathcal{N}(y | w^{\rm T} x, \beta^{-1})
$$&lt;/p&gt;
&lt;p&gt;似然函数反映的是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在参数 $w$ 已知的情况下，观测到数据 $y$ 的 “可能性” 有多大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;换句话说，它描述了数据的生成机制：&lt;/p&gt;
&lt;p&gt;$$
y = w^{\rm T} x + \epsilon
$$&lt;/p&gt;
&lt;p&gt;其中 $\epsilon$ 是噪声项，用来表示真实观测和理想模型之间的偏差。&lt;/p&gt;
&lt;p&gt;我们假设 $\epsilon \sim \mathcal{N}(0, \beta^{-1})$ ，于是可以得到：&lt;/p&gt;
&lt;p&gt;$$
y | x, w \sim \mathcal{N}(w^{\rm T} x, \beta^{-1})
$$&lt;/p&gt;
&lt;p&gt;那为什么先验分布要设成：&lt;/p&gt;
&lt;p&gt;$$
P(w) = \mathcal{N}(w | 0, \alpha^{-1} \mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;这里有 &lt;strong&gt;数学便利性&lt;/strong&gt; 和 &lt;strong&gt;建模含义&lt;/strong&gt; 两个方面的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数学便利性&lt;/p&gt;
&lt;p&gt;在贝叶斯推断中，我们要计算后验分布：&lt;/p&gt;
&lt;p&gt;$$
P(w | X, y) \propto P(y | X, w) P(w)
$$&lt;/p&gt;
&lt;p&gt;我们已经通过分析得到似然函数 $P(y | X, w)$ 服从高斯分布，如果我们将先验分布 $P(w)$ 也选择为高斯分布，那么先验函数和似然函数就满足 &lt;strong&gt;高斯共轭性（Gaussian Conjugacy）&lt;/strong&gt;，使得后验分布也会是高斯分布。&lt;/p&gt;
&lt;p&gt;这不仅简化了推导过程，还能得到后验的闭式解，避免数值近似或采样操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建模含义&lt;/p&gt;
&lt;p&gt;在没有观测数据之前，我们对参数 $w$ 的了解是模糊的。于是我们假设每个参数 $w_j$ 相互独立、均值为 0、方差有限：&lt;/p&gt;
&lt;p&gt;$$
w_j \sim \mathcal{N}(0, \alpha^{-1})
$$&lt;/p&gt;
&lt;p&gt;这表示我们对参数的 &lt;strong&gt;先验信念&lt;/strong&gt; ：模型应尽可能简单，除非数据强烈表明某个特征确实重要。&lt;/p&gt;
&lt;p&gt;在最大后验估计（MAP）框架下：&lt;/p&gt;
&lt;p&gt;$$
\arg\max_w P(w | X, y) = \arg\max_w P(y | X, w) P(w)
$$&lt;/p&gt;
&lt;p&gt;取对数后可以得到：&lt;/p&gt;
&lt;p&gt;$$
\arg\max_w \log P(y|X, w) - \frac{\alpha}{2} |w|^2
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这其实就对应了 &lt;strong&gt;L2 正则化&lt;/strong&gt; 的思想&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此从优化角度看，给参数 $w$ 加上高斯先验等价于在目标函数中加入 L2 正则化项。这意味着贝叶斯线性回归在建模阶段就自然地抑制了过大的参数，从而减少过拟合、提升泛化能力。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么优化核函数超参数的时候用的是边际似然而不是普通的似然函数？（什么是边际似然函数？）&lt;/p&gt;
&lt;p&gt;在高斯过程回归（GPR）中，我们希望根据观测数据学习核函数的超参数 $\theta$。直觉上，我们似乎可以最大化观测数据的似然。如果函数 $f$ 已知，则观测数据的似然为：&lt;/p&gt;
&lt;p&gt;$$
P(y|f) = \mathcal{N}(y|f, \beta^{-1}\mathbf{I})
$$&lt;/p&gt;
&lt;p&gt;然而问题在于：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们并不知道真实的 $f$ ，它本身是一个随机变量（随机过程）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此直接最大化这个 “条件似然” 是没意义的，因为 $f$ 是不确定的，我们根本没有一个确定的 $f$ 可以优化（极大似然估计假定背后的隐藏参数是固定的，这是 &lt;strong&gt;频率派的思想&lt;/strong&gt; ，而高斯过程假定隐藏函数是一个分布，这是 &lt;strong&gt;贝叶斯派思想&lt;/strong&gt; ）。&lt;/p&gt;
&lt;p&gt;既然我们无法确定出一个确定的 $f$ ，就要利用积分来将其边缘化：&lt;/p&gt;
&lt;p&gt;$$
P(y|X, \theta) = \int P(y|f) P(f|X, \theta) , df
$$&lt;/p&gt;
&lt;p&gt;这个积分结果被称为 &lt;strong&gt;边际似然（Marginal Likelihood）&lt;/strong&gt;，也常称为 &lt;strong&gt;模型证据（Model Evidence）&lt;/strong&gt;。它衡量了在给定核函数超参数 $\theta$ 的情况下，模型整体上对观测数据 $y$ 的解释能力。&lt;/p&gt;
&lt;p&gt;通过最大化边际似然，我们能够自动选择那些既能很好拟合数据、又不会过度复杂化模型的核参数，从而实现一种 &lt;strong&gt;贝叶斯式的自动正则化&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;贝叶斯线性回归&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/daunxx/article/details/51725086&quot;&gt;贝叶斯线性回归（Bayesian Linear Regression）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/question/22007264/answers/updated&quot;&gt;如何通俗地解释贝叶斯线性回归的基本原理？&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://andrewwang.rbind.io/courses/bayesian_statistics/notes/Ch7_h.pdf&quot;&gt;多元线性回归贝叶斯模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.vicayang.cc/Note-Bayesian-Linear-Regression/&quot;&gt;贝叶斯数据分析(七)——贝叶斯线性回归&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://weirping.github.io/blog/Bayesian-Probabilities-in-ML.html&quot;&gt;贝叶斯线性回归与贝叶斯逻辑回归&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高斯过程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://borgwang.github.io/ml/2019/07/28/gaussian-processes.html&quot;&gt;高斯过程 Gaussian Processes 原理、可视化及代码实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.superui.cc/machine-learning/gaussian-process/&quot;&gt;高斯过程 Gaussian Process&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.imyangty.com/note-ml2024fall/gaussian-process/&quot;&gt;24秋机器学习笔记-07-高斯过程&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/stable/auto_examples/gaussian_process/index.html&quot;&gt;【ScikitLearn】高斯过程用于机器学习&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高斯过程回归&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/HelloWorldTM/article/details/126980872&quot;&gt;高斯过程回归(Gaussian Processes Regression, GPR)简介&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/stable/modules/gaussian_process.html&quot;&gt;【ScikitLearn】高斯过程回归与高斯过程分类&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/v20000727/article/details/138086802&quot;&gt;高斯过程回归【详细数学推导】&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bookdown.org/xiangyun/masr/gaussian-processes-regression.html&quot;&gt;贝叶斯建模-高斯过程回归&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/697071644&quot;&gt;高斯过程回归（GPR）原理与实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sirlis.cn/posts/deep-learning-gaussian-process/&quot;&gt;深度学习基础（高斯过程）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基础算法】第一节：滤波算法</title><link>https://xingguang641.com/posts/filtering-algorithms/filtering-algorithms/</link><guid isPermaLink="true">https://xingguang641.com/posts/filtering-algorithms/filtering-algorithms/</guid><description>介绍机器学习常见的算法</description><pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;贝叶斯滤波框架介绍&lt;/h1&gt;
&lt;p&gt;从这一节开始，我们正式进入 &lt;strong&gt;滤波算法（Filtering Algorithms）&lt;/strong&gt; 的章节。提到滤波算法，就不得不先讲讲它的理论核心 ———— &lt;strong&gt;贝叶斯滤波（Bayesian Filtering）&lt;/strong&gt;。严格来说，贝叶斯滤波其实并不是一种具体的算法/模型，而是一种 &lt;strong&gt;通用的思想框架&lt;/strong&gt; 。它告诉我们：如果我们知道系统是如何变化的，以及观测数据与真实状态之间的关系，那么就能通过 “更新信念” 的方式，不断修正对当前状态的估计。&lt;/p&gt;
&lt;p&gt;但在正式讲解贝叶斯滤波之前，我们首先要了解什么是 &lt;strong&gt;滤波算法（Filtering Algorithms）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;想象这样一个场景：&lt;/p&gt;
&lt;p&gt;你正在研究一辆自动驾驶汽车。车上安装了 GPS、雷达、摄像头、惯性测量单元（IMU）等各种传感器。它们不停地输出关于车辆位置、速度、加速度的数据。&lt;/p&gt;
&lt;p&gt;看起来数据很充足对吧？但问题的关键是：这些观测都是 &lt;strong&gt;带噪声的&lt;/strong&gt; 。GPS 信号会漂移、雷达会误反射、IMU 也会随着时间累积误差，每个时刻的观测都是不完美的。也就是说：我们真正关心的车辆的真实状态是 &lt;strong&gt;被噪声隐藏起来的&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cfiltering-algorithms%5C%E6%BB%A4%E6%B3%A2%E7%AE%97%E6%B3%951.jpg&quot; alt=&quot;滤波算法图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;于是问题来了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当观测数据不可靠时，我们该如何尽可能准确地估计出系统的真实状态？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这正是滤波算法要解决的问题。&lt;/p&gt;
&lt;p&gt;“滤波” 这个名字其实很形象：就像在嘈杂的信号中筛出干净的部分一样，滤波算法的目标就是在充满噪声的不确定观测中，提取出最可能的真实状态。从概率论的角度来看，滤波本质上就是一种 &lt;strong&gt;去噪估计（Noise Reduction Estimation）&lt;/strong&gt;。在随机过程理论中，这种 “利用有噪观测去恢复隐藏状态” 的过程被称为 ———— &lt;strong&gt;滤波（Filtering）&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;滤波的数学定义（Mathematical Definition of Filtering）&lt;/h2&gt;
&lt;p&gt;至此我们已经直观地理解了滤波所要解决的事情，而要从理论上严谨地描述这一过程，我们就需要借助 &lt;strong&gt;统计估计理论（Statistical Estimation Theory）&lt;/strong&gt; 这个工具。&lt;/p&gt;
&lt;p&gt;在统计意义上，所谓估计就是根据观测数据去推测未知量的真实值。如果这种未知量是一个固定的常数，我们则称之为 &lt;strong&gt;参数估计问题&lt;/strong&gt; ；但如果未知量会随时间动态变化，那么这类问题就属于 &lt;strong&gt;动态估计问题（Dynamic Estimation Problem）&lt;/strong&gt;。滤波问题正是其中的一个经典应用场景。&lt;/p&gt;
&lt;p&gt;假设一个系统在任意时刻 $t$ 的内部状态（例如位置、速度、温度等）可以用一个向量表示，记作 $x_t$ ，它描述了系统在该时刻的 “真实但不可直接观测” 的状态。&lt;/p&gt;
&lt;p&gt;而我们能够直接获取的，是由传感器或测量设备提供的观测值，记作 $z_t$ ，这些观测值与真实状态相关，但通常包含噪声。&lt;/p&gt;
&lt;p&gt;于是，一个典型的动态系统可以由两类方程来描述：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
x_t = f(x_{t-1}, v_{t-1}) &amp;amp; \text{（状态转移方程）} \
\
z_t = h(x_t, w_t) &amp;amp; \text{（观测方程）}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f(\cdot)$ ：描述系统状态如何随时间演化（系统的 &lt;strong&gt;动态规律&lt;/strong&gt; ）&lt;/li&gt;
&lt;li&gt;$h(\cdot)$ ：描述系统状态如何被观测到（系统的 &lt;strong&gt;观测规律&lt;/strong&gt; ）&lt;/li&gt;
&lt;li&gt;$v_t$ 、$w_t$ ：分别代表系统噪声与观测噪声，通常假设它们相互独立&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换句话说，系统在每个时刻都会经历两个过程：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先变化（预测）$\longrightarrow$ 再被观测（更新）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;给定一段时间内的观测序列：&lt;/p&gt;
&lt;p&gt;$$
z_{1:t} = { z_1, z_2, \ldots, z_t }
$$&lt;/p&gt;
&lt;p&gt;我们的目标是估计在时刻 $t$ 的系统状态分布：&lt;/p&gt;
&lt;p&gt;$$
P(x_t|z_{1:t})
$$&lt;/p&gt;
&lt;p&gt;这就是滤波问题的数学定义：根据截至当前的所有观测信息，求系统当前状态的后验概率分布。&lt;/p&gt;
&lt;p&gt;需要注意的是：如果我们仅使用当前观测 $z_t$ 来估计状态，那只是 &lt;strong&gt;瞬时估计（Instantaneous Estimation）&lt;/strong&gt;。而滤波更强调的是 &lt;strong&gt;递推性（Recursiveness）&lt;/strong&gt;———— 随着时间的推移，不断地预测与更新，持续修正我们对系统状态的 “信念” 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们可以发现滤波问题的状态转移跟 HMM 状态转移非常像，这里借用 HMM 的概率图直观理解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cfiltering-algorithms%5C%E9%9A%90%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B1.png&quot; alt=&quot;滤波概率图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;贝叶斯估计（Bayesian Estimation）&lt;/h2&gt;
&lt;p&gt;通过前面的内容我们已经知道，滤波问题的目标，是希望利用带噪声的观测序列 $z_{1:t}$ 去估计系统的真实状态 $x_t$ 。而在概率论的语境下，这个 “估计” 的本质，其实就是计算一个条件概率分布 $P(x_t|z_{1:t})$ 。而要得到这个后验概率，最自然的工具就是 &lt;strong&gt;贝叶斯公式（Bayes&apos; Rule）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;贝叶斯估计的核心思想非常简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先根据已知的模型和历史观测得到一个先验信念（Prior），再根据新的观测信息对它进行修正（Likelihood），从而获得一个新的信念（Posterior）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用数学形式表示就是：&lt;/p&gt;
&lt;p&gt;$$
P(x_t|z_{1:t}) = \frac{P(z_t|x_t)P(x_t|z_{1:t-1})}{P(z_t|z_{1:t-1})}
$$&lt;/p&gt;
&lt;p&gt;这正是贝叶斯滤波更新步骤的数学基础。如果我们能递推地计算出上述分布，就能在每个时刻动态地更新对系统状态的估计。&lt;/p&gt;
&lt;h2&gt;递推贝叶斯状态估计（Recursive Bayesian State Estimation）&lt;/h2&gt;
&lt;p&gt;贝叶斯滤波，又称 &lt;strong&gt;递推贝叶斯状态估计（Recursive Bayesian State Estimation）&lt;/strong&gt;，其本质是贝叶斯估计 &lt;strong&gt;在时间序列上的递推形式&lt;/strong&gt; 。下面将给出其完整的推导过程。&lt;/p&gt;
&lt;p&gt;根据贝叶斯公式，可以把滤波过程拆解为两个核心步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预测（Prediction）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从全概率公式（对上一时刻状态积分/求和）出发：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(x_t | z_{1:t-1}) &amp;amp;= \int P(x_t, x_{t-1} | z_{1:t-1}) , dx_{t-1} \
&amp;amp;= \int P(x_t | x_{t-1}, z_{1:t-1}) , P(x_{t-1} | z_{1:t-1}) , dx_{t-1}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;再利用马尔可夫性假设 $P(x_t | x_{t-1}, z_{1:t-1}) = P(x_t | x_{t-1})$ ，可以得到：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t-1}) = \int P(x_t | x_{t-1}) , P(x_{t-1} | z_{1:t-1}) , dx_{t-1}
$$&lt;/p&gt;
&lt;p&gt;这个步骤可以理解为：“根据系统的动态规律，推测现在可能在哪里”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;更新（Update）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先直接用贝叶斯公式：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t}) = \frac{P(z_t | x_t, z_{1:t-1}) P(x_t | z_{1:t-1})}{P(z_t | z_{1:t-1})}
$$&lt;/p&gt;
&lt;p&gt;利用观测条件独立性 $P(z_t | x_t, z_{1:t-1}) = P(z_t | x_t)$ 将分子化简为 $P(z_t | x_t) P(x_t | z_{1:t-1})$ 。其中的归一化常数（分母）由全概率给出：&lt;/p&gt;
&lt;p&gt;$$
P(z_t | z_{1:t-1}) = \int P(z_t | x_t) P(x_t | z_{1:t-1}) , dx_t
$$&lt;/p&gt;
&lt;p&gt;因此得到标准更新公式：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t}) = \frac{P(z_t | x_t) P(x_t | z_{1:t-1})}{\int P(z_t | x_t) P(x_t | z_{1:t-1}) , dx_t}
$$&lt;/p&gt;
&lt;p&gt;这个步骤可以理解为：“根据当前的观测，修正我们的信念”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;贝叶斯滤波为 &lt;strong&gt;状态估计问题&lt;/strong&gt; 提供了最通用的理论框架。然而在实际应用中，该公式中的积分往往难以解析计算，尤其在 &lt;strong&gt;非线性、非高斯&lt;/strong&gt; 系统下几乎无法得到闭式解。因此尽管贝叶斯滤波在理论上极其完备，但在工程中通常使用近似的方法实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;卡尔曼滤波（Kalman Filter）&lt;/strong&gt; ———— 线性高斯系统的最优解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展卡尔曼滤波（EKF）&lt;/strong&gt; ———— 非线性系统的近似解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;粒子滤波（Particle Filter）&lt;/strong&gt; ———— 适用于任意非线性、非高斯系统的采样方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们将详细介绍这三个具体的滤波算法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;卡尔曼滤基本原理&lt;/h1&gt;
&lt;p&gt;在现实世界中，任何测量都伴随着噪声和不确定性。无论是追踪卫星轨迹、预测经济指标，还是实现自动驾驶汽车的精准定位，我们都需要一种方法从杂乱的数据中提取出真实的信号。&lt;strong&gt;卡尔曼滤波（Kalman Filter）&lt;/strong&gt; 正是为解决这一问题而诞生的强大工具。&lt;/p&gt;
&lt;p&gt;在 1960 年，卡尔曼发表了他著名的用递归方法解决离散数据线性滤波问题的论文。从那以后，得益于数字计算技术的进步，卡尔曼滤波器已经衍生出来多种版本的滤波器。&lt;/p&gt;
&lt;p&gt;卡尔曼滤波是一种高效率的递归滤波器（自回归滤波器），如果不以人名命名，则其名称是 &lt;strong&gt;线性二次估计（Linear Quadratic Estimation）&lt;/strong&gt;，它能够从一系列的不完全及包含噪声的测量中，估计动态系统的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cfiltering-algorithms%5C%E5%8D%A1%E5%B0%94%E6%9B%BC%E6%BB%A4%E6%B3%A21.jpg&quot; alt=&quot;卡尔曼滤波图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;先验假设（Prior Assumption）&lt;/h2&gt;
&lt;p&gt;在上面的贝叶斯滤波框架介绍我们知道：卡尔曼滤波是贝叶斯滤波在特定假设下的简化版本。或者更准确地说，是贝叶斯滤波在 &lt;strong&gt;线性高斯假设（Linear Gaussian Assumption）&lt;/strong&gt; 下的解析解。&lt;/p&gt;
&lt;p&gt;下面我们详细讲清楚这背后的前提假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;系统是线性的（Linear System）&lt;/p&gt;
&lt;p&gt;$$
x_t = Ax_{t-1} + Bu_t + w_t \quad z_t = Hx_t + v_t
$$&lt;/p&gt;
&lt;p&gt;这种线性结构保证了状态的演化与观测之间可以通过矩阵运算直接描述，使得贝叶斯滤波中的积分步骤能够被解析地求解，从而得到闭式递推公式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;噪声服从高斯分布（Gaussian Noise）&lt;/p&gt;
&lt;p&gt;$$
w_t \sim \mathcal{N}(0, Q) \quad v_t \sim \mathcal{N}(0, R)
$$&lt;/p&gt;
&lt;p&gt;其中 $w_t$ 为 &lt;strong&gt;过程噪声&lt;/strong&gt; （Process Noise）， $v_t$ 为 &lt;strong&gt;观测噪声&lt;/strong&gt; （Measurement Noise）。&lt;/p&gt;
&lt;p&gt;由于高斯分布在线性变换下仍保持高斯形式（闭合性），因此系统的先验与后验分布始终保持为高斯分布，可完全由均值和协方差刻画。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;噪声独立性假设（Independence Assumption）&lt;/p&gt;
&lt;p&gt;$$
P(w_t, v_t | x_{0:t-1}, z_{1:t-1}) = P(w_t),P(v_t)
$$&lt;/p&gt;
&lt;p&gt;这种独立性避免了联合分布中出现复杂的耦合项，大幅简化了贝叶斯推导的计算，使得滤波公式可以逐步递推。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始状态为高斯分布（Gaussian Prior）&lt;/p&gt;
&lt;p&gt;$$
x_0 \sim \mathcal{N}(\hat{x}_0, P_0)
$$&lt;/p&gt;
&lt;p&gt;假设初始状态是高斯分布，这样通过线性高斯系统的递推，整个状态序列的分布都会保持高斯形式，保证卡尔曼滤波在每个时刻都能用 “均值 + 协方差” 精确描述系统状态的不确定性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;在讲解贝叶斯滤波框架的时候我们已经给出了贝叶斯滤波的递推形式：&lt;/p&gt;
&lt;p&gt;$$
\left{
\begin{aligned}
&amp;amp;P(x_t | z_{1:t-1}) = \int P(x_t | x_{t-1}),P(x_{t-1} | z_{1:t-1}),dx_{t-1} &amp;amp;&amp;amp; \text{(预测步)} \
\[0.5em]
&amp;amp;P(x_t | z_{1:t}) \propto P(z_t | x_t),P(x_t | z_{1:t-1}) &amp;amp;&amp;amp; \text{(更新步)}
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;p&gt;我们已经知道整个滤波过程共分为预测和更新两个阶段。卡尔曼滤波正是在上述两步的基础上，利用线性高斯假设，将概率形式的积分与乘法转化为对 &lt;strong&gt;均值与协方差&lt;/strong&gt; 的递推计算。&lt;/p&gt;
&lt;h3&gt;Prediction Step&lt;/h3&gt;
&lt;p&gt;假设我们已经在时刻 $t-1$ 时获得了状态后验分布：&lt;/p&gt;
&lt;p&gt;$$
x_{t-1} | z_{t-1} \sim \mathcal{N}(\hat{x}&lt;em&gt;{t-1|t-1}, P&lt;/em&gt;{t-1|t-1})
$$&lt;/p&gt;
&lt;p&gt;因此直接可以得到：&lt;/p&gt;
&lt;p&gt;$$
P(x_{t-1}|z_{1:t-1}) = \mathcal{N}(\hat{x}&lt;em&gt;{t-1|t-1}, P&lt;/em&gt;{t-1|t-1})
$$&lt;/p&gt;
&lt;p&gt;根据系统状态方程：&lt;/p&gt;
&lt;p&gt;$$
x_t = Ax_{t-1} + Bu_t + w_t \quad w_t \sim \mathcal{N}(0, Q)
$$&lt;/p&gt;
&lt;p&gt;由于上述公式中出现的所有变量都服从高斯分布，因此 $x_t$ 在给定 $x_{t-1}$ 的情况下仍然是高斯分布（这里的 $u_t$ 是已知的控制输入，而 $x_{t-1}$ 是给定的条件，因此真正的变量只有 $w_t$ ，所以最终得到的分布的方差为 $Q$ ）：&lt;/p&gt;
&lt;p&gt;$$
x_t|x_{t-1} \sim \mathcal{N}(Ax_{t-1} + Bu_t, Q)
$$&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;p&gt;$$
P(x_t|x_{t-1}) = \mathcal{N}(Ax_{t-1} + Bu_t, Q)
$$&lt;/p&gt;
&lt;p&gt;根据贝叶斯滤波预测公式：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t-1}) = \int P(x_t | x_{t-1}),P(x_{t-1} | z_{1:t-1}),dx_{t-1}
$$&lt;/p&gt;
&lt;p&gt;因为高斯分布的线性卷积仍为高斯分布，所以 $P(x_t|z_{1:t-1})$ 也同样是高斯分布。因此我们可以直接推导 $P(x_t|z_{1:t-1})$ 的均值和协方差从而确定出分布 $P(x_t|z_{1:t-1})$ ：&lt;/p&gt;
&lt;p&gt;$$
P(x_t|z_{1:t-1}) = \mathcal{N}(\hat{x}&lt;em&gt;{t|t-1}, P&lt;/em&gt;{t|t-1})
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预测方差（Law of total expectation）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{align*}
\hat{x}&lt;em&gt;{t|t-1} &amp;amp;= \mathbb{E} \Big[ x_t | z&lt;/em&gt;{1:t-1} \Big] = \mathbb{E} \Big[ \mathbb{E} \big[ x_t | x_{t-1} \big] | z_{1:t-1} \Big] \
&amp;amp;= \mathbb{E} \Big[ Ax_{t-1} + Bu_t | z_{1:t-1} \Big] = A\mathbb{E} \Big[ x_{t-1} | z_{1:t-1} \Big] + Bu_t \
&amp;amp;= A\hat{x}_{t-1|t-1} + Bu_t
\end{align*}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预测协方差（Law of total covariance）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\begin{align*}
P_{t|t-1} &amp;amp;= \text{Cov} \Big( x_t | z_{1:t-1} \Big) = \mathbb{E} \Big[ \text{Cov} \big( x_t | x_{t-1} \big) | z_{1:t-1} \Big] + \text{Cov} \Big( \mathbb{E} \big[ x_t | x_{t-1} \big] | z_{1:t-1} \Big) \
&amp;amp;= \mathbb{E} \Big[ Q | z_{1:t-1} \Big] + \text{Cov} \Big( Ax_{t-1} + Bu_t | z_{1:t-1} \Big) = Q + A , \text{Cov} \Big( x_{t-1} | z_{1:t-1} \Big) , A^{\rm T} \
&amp;amp;= Q + AP_{t-1|t-1}A^{\rm T}
\end{align*}
$$&lt;/p&gt;
&lt;h3&gt;Update Step&lt;/h3&gt;
&lt;p&gt;通过 Prediction Step 我们可以得到先验分布为：&lt;/p&gt;
&lt;p&gt;$$
p(x_t | z_{1:t-1}) =
\frac{1}{(2\pi)^{\frac{n}{2}} |P_{t|t-1}|^{\frac{1}{2}}}
\exp \Big( -\frac{1}{2} (x_t - \hat{x}&lt;em&gt;{t|t-1})^{\rm T} P&lt;/em&gt;{t|t-1}^{-1} (x_t - \hat{x}_{t|t-1}) \Big)
$$&lt;/p&gt;
&lt;p&gt;当新的观测值 $z_t$ 到达时，我们利用观测方程：&lt;/p&gt;
&lt;p&gt;$$
z_t = Hx_t + v_t \quad v_t \sim \mathcal{N}(0, R)
$$&lt;/p&gt;
&lt;p&gt;因此可以得到观测似然为：&lt;/p&gt;
&lt;p&gt;$$
p(z_t | x_t) =
\frac{1}{(2\pi)^{\frac{m}{2}} |R|^{\frac{1}{2}}}
\exp \Big( -\frac{1}{2} (z_t - H x_t)^{\rm T} R^{-1} (z_t - H x_t) \Big)
$$&lt;/p&gt;
&lt;p&gt;根据贝叶斯滤波更新公式可得：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
P(x_t | z_{1:t})
&amp;amp;\propto
\exp \Big( -\frac{1}{2} (x_t - \hat{x}&lt;em&gt;{t|t-1})^{\rm T} P&lt;/em&gt;{t|t-1}^{-1} (x_t - \hat{x}&lt;em&gt;{t|t-1}) \Big)
\exp \Big( -\frac{1}{2} (z_t - H x_t)^{\rm T} R^{-1} (z_t - H x_t) \Big) \
&amp;amp;= \exp \Bigg( -\frac{1}{2} \Big[ x_t^{\rm T} P&lt;/em&gt;{t|t-1}^{-1} x_t - 2 x_t^{\rm T} P_{t|t-1}^{-1} \hat{x}_{t|t-1} + (z_t - H x_t)^{\rm T} R^{-1} (z_t - H x_t) \Big] \Bigg)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;现在想办法将上述公式化简成如下形式：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t}) \propto
\exp \Big( -\frac{1}{2} (x_t - \hat{x}&lt;em&gt;{t|t})^{\rm T} P&lt;/em&gt;{t|t}^{-1} (x_t - \hat{x}_{t|t}) \Big)
$$&lt;/p&gt;
&lt;p&gt;将指数项最后一项展开：&lt;/p&gt;
&lt;p&gt;$$
(z_t - H x_t)^{\rm T} R^{-1} (z_t - H x_t) = x_t^{\rm T} H^{\rm T} R^{-1} H x_t - 2 x_t^{\rm T} H^{\rm T} R^{-1} z_t + z_t^{\rm T} R^{-1} z_t
$$&lt;/p&gt;
&lt;p&gt;代回指数项整理可得：&lt;/p&gt;
&lt;p&gt;$$
x_t^{\rm T} (P_{t|t-1}^{-1} + H^{\rm T} R^{-1} H) x_t - 2 x_t^{\rm T} (P_{t|t-1}^{-1} \hat{x}_{t|t-1} + H^{\rm T} R^{-1} z_t) + z_t^{\rm T} R^{-1} z_t
$$&lt;/p&gt;
&lt;p&gt;对比二次型形式 $x_t^{\rm T} a x_t - 2 x_t^{\rm T} b$ ，忽略常数项（不含 $x_t$ 的项）后不难得出：&lt;/p&gt;
&lt;p&gt;$$
a = P_{t|t-1}^{-1} + H^{\rm T} R^{-1} H \quad b = P_{t|t-1}^{-1} \hat{x}_{t|t-1} + H^{\rm T} R^{-1} z_t
$$&lt;/p&gt;
&lt;p&gt;综合上述信息不难得到：&lt;/p&gt;
&lt;p&gt;$$
P_{t|t} = (P_{t|t-1}^{-1} + H^{\rm T} R^{-1} H)^{-1} \quad \hat{x}&lt;em&gt;{t|t} = P&lt;/em&gt;{t|t} \big( P_{t|t-1}^{-1} \hat{x}_{t|t-1} + H^{\rm T} R^{-1} z_t \big)
$$&lt;/p&gt;
&lt;p&gt;接下来重复 Prediction Step 直至循环结束即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;卡尔曼滤波代码讲解&lt;/h1&gt;
&lt;p&gt;卡尔曼滤波算法的核心非常简洁，本质上就是在上述推导过程中得到的两个步骤 ———— 预测与更新 ———— 的递推公式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

class KalmanFilter:
    def __init__(self, A, B, H, Q, R, x0, P0):
        self.A = A; self.B = B; self.Q = Q
        self.H = H; self.R = R

        self.x = x0 # 后验均值
        self.P = P0 # 后验协方差

    def predict(self, u=None):
        if u is None:
            u = np.zeros((self.B.shape[1],))
        # 预测状态均值
        self.x = self.A @ self.x + self.B @ u
        # 预测协方差
        self.P = self.A @ self.P @ self.A.T + self.Q
        return self.x, self.P

    def update(self, z):
        # 计算卡尔曼增益
        S = self.H @ self.P @ self.H.T + self.R
        K = self.P @ self.H.T @ np.linalg.inv(S)
        # 更新状态均值
        y = z - self.H @ self.x # 观测残差
        self.x = self.x + K @ y
        # 更新协方差
        I = np.eye(self.P.shape[0])
        self.P = (I - K @ self.H) @ self.P
        return self.x, self.P, K
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;扩展卡尔曼滤基本原理&lt;/h1&gt;
&lt;p&gt;在前一节中，我们介绍了 &lt;strong&gt;卡尔曼滤波&lt;/strong&gt; ，它是一种针对线性系统的递推状态估计方法，通过预测和更新两步迭代实现对系统状态的高效估计。然而，在实际工程中，绝大多数系统具有明显的非线性特性，例如飞行器的姿态控制、移动机器人路径规划以及非线性传感器测量系统。传统的线性卡尔曼滤波器在处理这些非线性系统时可能产生较大的误差，甚至无法收敛。&lt;/p&gt;
&lt;p&gt;为了解决这一问题，Schmidt 等学者提出了 &lt;strong&gt;扩展卡尔曼滤波器&lt;/strong&gt;（Extended Kalman Filter，简称 EKF）。扩展卡尔曼滤波器的核心思想是：在每个时间步，将非线性系统在当前估计点附近进行局部线性化处理，然后沿用线性卡尔曼滤波的预测和更新步骤来估计系统状态。通过这种方式，EKF 能够在保持递推效率的同时，处理非线性系统的状态估计问题。&lt;/p&gt;
&lt;p&gt;相比于线性卡尔曼滤波器，扩展卡尔曼滤波器具有更广泛的适用范围和更高的状态估计精度，同时可以适应不同频率的观测更新。因此，EKF 在航空航天、机器人导航、自动驾驶以及金融建模等领域得到了广泛应用，成为解决非线性系统状态估计问题的重要工具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cfiltering-algorithms%5C%E6%89%A9%E5%B1%95%E5%8D%A1%E5%B0%94%E6%9B%BC%E6%BB%A4%E6%B3%A21.jpg&quot; alt=&quot;扩展卡尔曼滤波图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;扩展卡尔曼滤波旨在解决非线性系统的状态估计问题，其状态空间方程可以表示为：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
x_t = f(x_{t-1}, u_t) + w_t \quad &amp;amp; w_t \sim \mathcal{N}(0, Q) \
\
z_t = h(x_t) + v_t \quad &amp;amp; v_t \sim \mathcal{N}(0, R)
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;要将卡尔曼滤波应用于非线性系统，核心步骤是对非线性系统进行线性化处理。扩展卡尔曼滤波通过在当前状态估计点对系统方程进行 &lt;strong&gt;一阶泰勒展开&lt;/strong&gt; 来实现局部线性化，从而在每个时间步近似为线性系统进行预测与更新。&lt;/p&gt;
&lt;p&gt;扩展卡尔曼滤波是一个二元向量输入多输出系统。对于对于一个二元输入、多输出的非线性系统函数有如下两个公式需要了解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单输出的二元函数 $f(x, y)$ 在点 $(x_0, y_0)$ 处的一阶泰勒展开公式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
f(x, y) \approx f(x_0, y_0) + \frac{\partial f}{\partial x}\Big|&lt;em&gt;{(x_0, y_0)} (x - x_0) + \frac{\partial f}{\partial y}\Big|&lt;/em&gt;{(x_0, y_0)} (y - y_0)
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113553197499694&amp;amp;bvid=BV1WvBQYsEkL&amp;amp;cid=27052212975&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多输出函数对输入向量求导公式（雅可比矩阵）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathbf{J}_{\mathbf{f}}(\mathbf{x}) =
\frac{\partial \mathbf{f}}{\partial \mathbf{x}} =
\begin{bmatrix}
\dfrac{\partial f_1}{\partial x_1} \quad &amp;amp; \dfrac{\partial f_1}{\partial x_2} \quad &amp;amp; \cdots \quad &amp;amp; \dfrac{\partial f_1}{\partial x_n} \[6pt]
\dfrac{\partial f_2}{\partial x_1} \quad &amp;amp; \dfrac{\partial f_2}{\partial x_2} \quad &amp;amp; \cdots \quad &amp;amp; \dfrac{\partial f_2}{\partial x_n} \[6pt]
\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots \[6pt]
\dfrac{\partial f_m}{\partial x_1} \quad &amp;amp; \dfrac{\partial f_m}{\partial x_2} \quad &amp;amp; \cdots \quad &amp;amp; \dfrac{\partial f_m}{\partial x_n}
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=966638278&amp;amp;bvid=BV1DW4y1F7gB&amp;amp;cid=28736554042&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;对非线性状态函数 $f(x_{t-1}, u_t)$ 在当前状态估计 $\hat{x}_{t-1|t-1}$ 附近进行一阶展开：&lt;/p&gt;
&lt;p&gt;$$
f(x_{t-1}, u_t) \approx f(\hat{x}&lt;em&gt;{t-1|t-1}, u_t) + F&lt;/em&gt;{t-1} (x_{t-1} - \hat{x}_{t-1|t-1})
$$&lt;/p&gt;
&lt;p&gt;其中 $F_{t-1}$ 是状态方程的雅可比矩阵。&lt;/p&gt;
&lt;p&gt;同理对观测函数 $h(x_t)$ 在先验估计 $\hat{x}_{t|t-1}$ 附近进行一阶展开：&lt;/p&gt;
&lt;p&gt;$$
h(x_t) \approx h(\hat{x}&lt;em&gt;{t|t-1}) + H_t (x_t - \hat{x}&lt;/em&gt;{t|t-1})
$$&lt;/p&gt;
&lt;p&gt;其中 $H_{t}$ 是观测方程的雅可比矩阵。&lt;/p&gt;
&lt;p&gt;通过上述线性化，原本的非线性系统就被局部近似线性系统：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
x_t \approx f(\hat{x}&lt;em&gt;{t-1|t-1}, u_t) + F&lt;/em&gt;{t-1} (x_{t-1} - \hat{x}&lt;em&gt;{t-1|t-1}) + w_t \quad &amp;amp; w_t \sim \mathcal{N}(0, Q) \
\
z_t \approx h(\hat{x}&lt;/em&gt;{t|t-1}) + H_t (x_t - \hat{x}_{t|t-1}) + v_t \quad &amp;amp; v_t \sim \mathcal{N}(0, R)
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;引入 &lt;strong&gt;状态偏移量&lt;/strong&gt; 和 &lt;strong&gt;观测偏移量&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\bar{x}&lt;em&gt;{t-1} = x&lt;/em&gt;{t-1} - \hat{x}_{t-1|t-1} \quad \bar{x}&lt;em&gt;t = x_t - f(\hat{x}&lt;/em&gt;{t-1|t-1}, u_t)
$$&lt;/p&gt;
&lt;p&gt;$$
\bar{x}&lt;em&gt;t = x_t - \hat{x}&lt;/em&gt;{t|t-1} \quad \bar{z}&lt;em&gt;t = z_t - h(\hat{x}&lt;/em&gt;{t|t-1})
$$&lt;/p&gt;
&lt;p&gt;其中 $\hat{x}&lt;em&gt;{t-1|t-1}$ 、 $f(\hat{x}&lt;/em&gt;{t-1|t-1}, u_t)$ 和 $h(\hat{x}&lt;em&gt;{t|t-1})$ 都是已知的，并且还有 $\hat{x}&lt;/em&gt;{t|t-1} = f(\hat{x}_{t-1|t-1}, u_t)$ 。&lt;/p&gt;
&lt;p&gt;经过简单的变形可得：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
\bar{x}&lt;em&gt;t = F&lt;/em&gt;{t-1} \bar{x}_{t-1} + w_t \quad &amp;amp; w_t \sim \mathcal{N}(0, Q) \
\
\bar{z}_t = H_t \bar{x}_t + v_t \quad &amp;amp; v_t \sim \mathcal{N}(0, R)
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;这样就可以 &lt;strong&gt;直接套用线性卡尔曼滤波的预测和更新公式&lt;/strong&gt; ，只是需要用雅可比矩阵 $F_{t-1}$ 和 $H_t$ 替代原来的线性矩阵 $A$ 和 $H$ ，并在每个时间步重新计算。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;扩展卡尔曼滤波代码讲解&lt;/h1&gt;
&lt;p&gt;EKF 是对非线性系统的状态估计方法，其核心思想与线性卡尔曼滤波类似：在每个时间步进行 &lt;strong&gt;预测&lt;/strong&gt; 和 &lt;strong&gt;更新&lt;/strong&gt; 两个递推步骤。不同的是 EKF 对非线性状态和观测函数进行了局部线性化处理，通过雅可比矩阵近似系统的线性行为。&lt;/p&gt;
&lt;p&gt;下面的代码展示了一个 EKF 的完整实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

class ExtendedKalmanFilter:
    def __init__(self, f, F_jacobian, h, H_jacobian, Q, R, x0, P0):
        self.f = f; self.F_jacobian = F_jacobian; self.Q = Q
        self.h = h; self.H_jacobian = H_jacobian; self.R = R

        self.x = x0 # 后验均值
        self.P = P0 # 后验协方差

    def predict(self, u=None):
        if u is None:
            u = np.zeros((1,))
        # 状态预测
        self.x = self.f(self.x, u)
        # 协方差预测
        F = self.F_jacobian(self.x, u)
        self.P = F @ self.P @ F.T + self.Q
        return self.x, self.P

    def update(self, z):
        # 雅可比矩阵
        H = self.H_jacobian(self.x)
        # 卡尔曼增益
        S = H @ self.P @ H.T + self.R
        K = self.P @ H.T @ np.linalg.inv(S)
        # 状态更新
        y = z - self.h(self.x)
        self.x = self.x + K @ y
        # 协方差更新
        I = np.eye(self.P.shape[0])
        self.P = (I - K @ H) @ self.P
        return self.x, self.P, K
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;粒子滤波基本原理&lt;/h1&gt;
&lt;p&gt;在非线性系统中，贝叶斯滤波面临的主要困难是 &lt;strong&gt;状态分布的表达和积分计算&lt;/strong&gt; 。对于一般的 &lt;strong&gt;非线性、非高斯&lt;/strong&gt; 系统，解析求解几乎不可行。为此人们引入了基于数值近似的 &lt;strong&gt;蒙特卡罗方法&lt;/strong&gt; ，其中最具代表性的就是 &lt;strong&gt;粒子滤波&lt;/strong&gt;（Particle Filter，简称 PF）。&lt;/p&gt;
&lt;p&gt;粒子滤波通过一组带权重的随机样本（称为 “粒子” ）来近似系统状态的后验分布，并在每个时刻根据观测信息更新粒子的分布和权重。由于不依赖线性化或高斯假设，它能够处理任意形式的非线性与非高斯系统，因此广泛应用于 &lt;strong&gt;机器人定位、目标跟踪、计算机视觉&lt;/strong&gt; 等领域。&lt;/p&gt;
&lt;p&gt;直观地说，粒子滤波就像在迷雾中寻找宝藏：每个粒子代表一个可能的位置，而其权重反映了该位置与观测结果的匹配程度。随着观测不断更新，粒子会逐渐集中到更可能的区域，从而逼近真实的状态分布。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cfiltering-algorithms%5C%E7%B2%92%E5%AD%90%E6%BB%A4%E6%B3%A21.jpg&quot; alt=&quot;粒子滤波图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;蒙特卡洛近似（Monte Carlo Approximation）&lt;/h2&gt;
&lt;p&gt;当系统的状态转移或观测模型存在强非线性或非高斯噪声时，贝叶斯滤波往往无法解析求解。为此粒子滤波引入了蒙特卡罗方法来对后验分布进行数值近似。&lt;/p&gt;
&lt;p&gt;设函数 $g(x)$ 关于分布 $P(x)$ 的期望为：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E} \Big[ g(x) \Big] = \int g(x) P(x) , dx
$$&lt;/p&gt;
&lt;p&gt;若能够从分布 $P(x)$ 中独立采样得到 $N$ 个样本 ${x^{(i)}}_{i=1}^N$，则该期望可以用样本均值近似为：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E} \Big[ g(x) \Big] \approx \frac{1}{N} \sum_{i=1}^{N} g(x^i)
$$&lt;/p&gt;
&lt;p&gt;当 $N \to \infty$ 时，根据 &lt;strong&gt;大数定律&lt;/strong&gt; ，该近似将收敛于真实的期望值。&lt;/p&gt;
&lt;h2&gt;序贯重要性采样（Sequential Importance Sampling）&lt;/h2&gt;
&lt;p&gt;在实际问题中，我们通常无法直接从后验分布 $P(x_t | z_{1:t})$ 中采样。为此粒子滤波采用了 &lt;strong&gt;重要性采样（Importance Sampling）&lt;/strong&gt; 的思想（具体介绍会在后续的章节中给出）：从一个更容易采样的 &lt;strong&gt;提议分布&lt;/strong&gt; $Q(x_t | x_{t-1}, z_t)$ 中生成样本（粒子），再通过加权修正来逼近真实后验分布。&lt;/p&gt;
&lt;p&gt;在序贯估计问题中，我们考虑完整的状态轨迹 $x_{0:t}$ ，目标是从后验分布 $P(x_{0:t} | z_{1:t})$ 中抽样。引入提议分布 $Q(x_{0:t} | z_{1:t})$ ，则其重要性权重定义为：&lt;/p&gt;
&lt;p&gt;$$
\lambda_t = \frac{P(x_{0:t} | z_{1:t})}{Q(x_{0:t} | z_{1:t})}
$$&lt;/p&gt;
&lt;p&gt;若状态满足马尔可夫性质、观测满足条件独立性，则有：&lt;/p&gt;
&lt;p&gt;$$
P(x_{0:t} | z_{1:t}) \propto P(z_t | x_t) P(x_t | x_{t-1}) P(x_{0:t-1} | z_{1:t-1})
$$&lt;/p&gt;
&lt;p&gt;将两式结合，可得权重的递推形式：&lt;/p&gt;
&lt;p&gt;$$
\lambda_t = \frac{P(z_t | x_t) P(x_t | x_{t-1})}{Q(x_t | x_{0:t-1}, z_{1:t})} \lambda_{t-1}
$$&lt;/p&gt;
&lt;p&gt;在常见的设置中，我们通常选用状态转移概率作为提议分布，即 $Q(x_t | x_{0:t-1}, z_{1:t}) = P(x_t | x_{t-1})$ ，此时权重更新公式可简化为：&lt;/p&gt;
&lt;p&gt;$$
\lambda_t = P(z_t | x_t) \lambda_{t-1}
$$&lt;/p&gt;
&lt;p&gt;最后对所有粒子的权重进行归一化：&lt;/p&gt;
&lt;p&gt;$$
\bar{\lambda}&lt;em&gt;t^i = \frac{\lambda_t^i}{\sum&lt;/em&gt;{j=1}^{N} \lambda_t^j}
$$&lt;/p&gt;
&lt;h2&gt;FPK 方程（Fokker–Planck Equation）&lt;/h2&gt;
&lt;p&gt;在连续时间的动态系统中，系统状态的演化通常可以用 &lt;strong&gt;随机微分方程&lt;/strong&gt;（Stochastic Differential Equation，简称SDE）来描述：&lt;/p&gt;
&lt;p&gt;$$
dx_t = f(x_t, t) dt + G(x_t, t) dW_t
$$&lt;/p&gt;
&lt;p&gt;其中 $f(x_t, t)$ 表示系统的漂移项（drift term）， $G(x_t, t)$ 为扩散系数矩阵（diffusion matrix）， $W_t$ 是 &lt;strong&gt;维纳过程（Wiener Process）&lt;/strong&gt;，用于描述系统中的随机扰动。&lt;/p&gt;
&lt;p&gt;对应于上述随机过程，系统状态的概率密度函数 $P(x, t)$ 满足 Fokker–Planck–Kolmogorov（FPK）方程：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial P(x,t)}{\partial t} = -\sum_{i=1}^{n} \frac{\partial}{\partial x_i} \Big[ f_i(x,t) P(x,t) \Big] + \frac{1}{2} \sum_{i=1}^{n} \sum_{j=1}^{n} \frac{\partial^2}{\partial x_i \partial x_j} \Big[ (GG^{\rm T})_{ij} P(x,t) \Big]
$$&lt;/p&gt;
&lt;p&gt;第一项表示 &lt;strong&gt;漂移项的影响&lt;/strong&gt;（由系统动力学 $f(x,t)$ 导致的概率流动），第二项则反映 &lt;strong&gt;扩散项的影响&lt;/strong&gt;（由噪声传播引起的概率扩散）。&lt;/p&gt;
&lt;p&gt;FPK 方程描述了系统状态概率密度在时间上的演化过程。然而在实际问题中直接求解该偏微分方程往往十分困难，尤其是当系统维度较高时。&lt;/p&gt;
&lt;p&gt;粒子滤波的核心思想正是通过蒙特卡洛方法来近似求解这一方程：通过在状态空间中生成大量样本（粒子）并随时间演化，用样本的加权分布去逼近 $P(x,t)$ 的动态变化，从而实现对系统状态的估计（这里并不需要完全了解什么是随机微分方程，我们会在扩散模型章节详细讲解这部分的内容）。&lt;/p&gt;
&lt;h2&gt;理论基础（Theoretical Foundation）&lt;/h2&gt;
&lt;p&gt;粒子滤波建立在贝叶斯滤波框架之上，因此同样包含贝叶斯滤波的两个核心步骤：&lt;strong&gt;预测（Prediction）&lt;/strong&gt; 与 &lt;strong&gt;更新（Update）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然而，与传统的解析贝叶斯滤波不同，粒子滤波引入了蒙特卡洛方法与重要性采样技术来对复杂的非线性、非高斯分布进行数值近似。为了克服粒子退化问题并获得更稳定的估计结果，粒子滤波在此基础上又增加了 &lt;strong&gt;重采样（Resampling）&lt;/strong&gt; 与 &lt;strong&gt;状态估计（Estimation）&lt;/strong&gt; 步骤。&lt;/p&gt;
&lt;p&gt;接下来，我们将对粒子滤波的完整推导过程进行详细说明。&lt;/p&gt;
&lt;h3&gt;Initialization Step&lt;/h3&gt;
&lt;p&gt;粒子滤波的第一步是 &lt;strong&gt;初始化（Initialization）&lt;/strong&gt;，其目标是根据系统的先验分布 $P(x_0)$ 生成初始粒子集合，用以表示系统在初始时刻的状态不确定性。&lt;/p&gt;
&lt;p&gt;我们从先验分布中采样得到 $N$ 个粒子（具体粒子个数由人工设定）：&lt;/p&gt;
&lt;p&gt;$$
x_0^{(i)} \sim P(x_0) \quad i = 1, 2, \ldots, N
$$&lt;/p&gt;
&lt;p&gt;每个粒子代表系统在状态空间中的一个可能位置。由于在初始时刻通常没有观测信息可用于修正先验分布，因此所有粒子的初始权重均相等：&lt;/p&gt;
&lt;p&gt;$$
\lambda_0^{(i)} = \frac{1}{N}
$$&lt;/p&gt;
&lt;p&gt;此时整组粒子 ${x_0^{(i)}, \lambda_0^{(i)}}_{i=1}^N$ 就构成了对初始状态分布 $P(x_0)$ 的离散近似：&lt;/p&gt;
&lt;p&gt;$$
P(x_0) \approx \sum_{i=1}^{N} \lambda_0^{(i)} \delta(x_0 - x_0^{(i)})
$$&lt;/p&gt;
&lt;p&gt;其中 $\delta(\cdot)$ 为狄拉克 delta 函数，表示概率质量集中在粒子所在的位置（可以参考下方视频了解其直观含义）。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114646350566949&amp;amp;bvid=BV1ZCTDz6E15&amp;amp;cid=30384260188&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;通过这种方式，连续的概率分布被一组带权样本所替代，为后续的预测与更新步骤提供了基础。&lt;/p&gt;
&lt;h3&gt;Prediction Step&lt;/h3&gt;
&lt;p&gt;假设我们已经得到了上一个时刻的后验分布的离散近似：&lt;/p&gt;
&lt;p&gt;$$
P(x_{t-1} | z_{1:t-1}) \approx \sum_{i=1}^{N} \lambda_{t-1}^{(i)} \delta(x_{t-1} - x_{t-1}^{(i)})
$$&lt;/p&gt;
&lt;p&gt;将上述离散近似公式带入预测公式：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(x_t | z_{1:t-1}) &amp;amp;= \int P(x_t | x_{t-1}) \sum_{i=1}^{N} \lambda_{t-1}^{(i)} \delta(x_{t-1} - x_{t-1}^{(i)}) , dx_{t-1} \
&amp;amp;= \sum_{i=1}^{N} \lambda_{t-1}^{(i)} \int P(x_t | x_{t-1}) \delta(x_{t-1} - x_{t-1}^{(i)}) , dx_{t-1} \
&amp;amp;= \sum_{i=1}^{N} \lambda_{t-1}^{(i)} P(x_t | x_{t-1}^{(i)})
\end{align*}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个公式告诉我们：预测分布可以看成上一时刻每个粒子通过系统模型推进后的状态分布的加权和。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而我们 &lt;strong&gt;并不需要&lt;/strong&gt; 显式求解出 “上一时刻每个粒子通过系统模型推进后的状态分布” 后再对这个分布采样来得到 $x_t^{(i)}$ （下面的推导是预测步骤的核心关键点）。&lt;/p&gt;
&lt;p&gt;因为上一时刻的粒子 $x_{t-1}^{(i)}$ 已经是已知的样本点，根据系统状态方程：&lt;/p&gt;
&lt;p&gt;$$
x_t^{(i)} = f(x_{t-1}^{(i)}) + w_t^{(i)}
$$&lt;/p&gt;
&lt;p&gt;我们可以当前状态 $x_t$ 的随机性完全来自过程噪声 $w_t^{(i)}$ ，因此我们只需要将原本的粒子 &lt;strong&gt;做一次非线性变换后再随机加噪&lt;/strong&gt; 就可以得到新粒子 $x_t^{(i)}$ 。
这些新粒子等价于从条件分布 $P(x_t | x_{t-1}^{(i)})$ 中采样得到的样本。&lt;/p&gt;
&lt;p&gt;换言之，每一个新粒子都代表一个条件分布 $P(x_t | x_{t-1}^{(i)})$ ，所有的新粒子及其对应的权重（当前阶段的权重沿用上一次迭代的结果）共同构成了预测分布 $P(x_t | z_{1:t-1})$ 的离散近似。&lt;/p&gt;
&lt;h3&gt;Update Step&lt;/h3&gt;
&lt;p&gt;预测步骤给出了系统在时刻 $t$ 的先验分布的离散近似：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t-1}) \approx \sum_{i=1}^{N} \lambda_{t-1}^{(i)} \delta(x_t - x_t^{(i)})
$$&lt;/p&gt;
&lt;p&gt;此时新的观测量 $z_t$ 到达，我们希望利用该观测信息对预测结果进行修正，从而得到更接近真实状态的后验分布。&lt;/p&gt;
&lt;p&gt;根据贝叶斯滤波更新公式可得：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t}) \propto \sum_{i=1}^{N} \lambda_{t-1}^{(i)} P(z_t | x_t^{(i)}) \delta(x_t - x_t^{(i)})
$$&lt;/p&gt;
&lt;p&gt;这意味着每个粒子根据其与观测的一致程度（即观测似然）获得新的权重，权重更新公式为：&lt;/p&gt;
&lt;p&gt;$$
\lambda_t^{(i)} = \lambda_{t-1}^{(i)} P(z_t | x_t^{(i)})
$$&lt;/p&gt;
&lt;p&gt;根据系统观测方程：&lt;/p&gt;
&lt;p&gt;$$
z_t = h(x_t) + v_t
$$&lt;/p&gt;
&lt;p&gt;在给定 $x_t$ 的情况下，当前观测 $z_t$ 的随机性完全来自观测噪声 $v_t$ ，因此可以直接写出观测似然 $P(z_t | x_t)$ ：&lt;/p&gt;
&lt;p&gt;$$
p(z_t | x_t) =
\frac{1}{(2\pi)^{\frac{m}{2}} |R|^{\frac{1}{2}}}
\exp \Big( -\frac{1}{2} (z_t - h(x_t))^{\rm T} R^{-1} (z_t - h(x_t)) \Big)
$$&lt;/p&gt;
&lt;p&gt;然后直接代入每个 Prediction Step 得到的粒子参数计算出新的权重，最后还需要进行归一化操作：&lt;/p&gt;
&lt;p&gt;$$
\bar{\lambda}&lt;em&gt;t^{(i)} = \frac{\lambda_t^{(i)}}{\sum&lt;/em&gt;{j=1}^{N} \lambda_t^{(j)}}
$$&lt;/p&gt;
&lt;h3&gt;Resampling Step&lt;/h3&gt;
&lt;p&gt;经过预测和更新后，粒子权重可能出现 &lt;strong&gt;退化现象&lt;/strong&gt;：大部分粒子的权重非常小，只有少数粒子权重占据主导。&lt;/p&gt;
&lt;p&gt;这会导致计算效率低下，甚至状态估计失真。为了解决这个问题，需要进行 &lt;strong&gt;重采样（Resampling）&lt;/strong&gt;，保留高权重粒子，舍弃低权重粒子，同时恢复粒子数量。&lt;/p&gt;
&lt;p&gt;假设我们在更新步骤后得到了权重归一化的粒子集合为 ${x_t^{(i)}, \bar{\lambda}&lt;em&gt;t^{(i)}}&lt;/em&gt;{i=1}^N$ 。&lt;/p&gt;
&lt;p&gt;然后我们根据归一化权重 $\bar{\lambda}_t^{(i)}$ 构造离散概率分布，并从该分布中 &lt;strong&gt;有放回&lt;/strong&gt; 地采样 $N$ 个粒子得到新的粒子集合，权重全部重新设置为 $\frac{1}{N}$ 。&lt;/p&gt;
&lt;p&gt;接下来重复 Prediction Step 直至循环结束即可。&lt;/p&gt;
&lt;h2&gt;渐进性分析（Asymptotic Analysis）&lt;/h2&gt;
&lt;p&gt;粒子滤波使用 $N$ 个粒子对后验分布 $P(x_t|z_{1:t})$ 进行离散近似：&lt;/p&gt;
&lt;p&gt;$$
P(x_t | z_{1:t}) \approx \sum_{i=1}^{N} w_t^i \delta(x_t - x_t^i)
$$&lt;/p&gt;
&lt;p&gt;对任意可积函数 $\phi$ ，用 $\displaystyle \hat{\phi}&lt;em&gt;N = \sum&lt;/em&gt;{i=1}^{N} w_t^i \phi(x_t^i)$ 作为 $\displaystyle \int \phi(x) , P(x | z_{1:t}) , dx$ 的蒙特卡洛估计。&lt;/p&gt;
&lt;p&gt;定义蒙特卡洛误差：&lt;/p&gt;
&lt;p&gt;$$
\varepsilon_N = \hat{\phi}&lt;em&gt;N - \int \phi(x) P(x | z&lt;/em&gt;{1:t}) dx
$$&lt;/p&gt;
&lt;p&gt;如果粒子通过最优提议分布采样，并且经过重采样去掉权重偏差，可以将 ${\phi(x_t^i)}$ 看作独立同分布样本。&lt;/p&gt;
&lt;p&gt;根据中心极限定理：&lt;/p&gt;
&lt;p&gt;$$
\sqrt{N} \varepsilon_N = \sqrt{N} \left( \hat{\phi}&lt;em&gt;N - \mathbb{E}[\phi(x_t)] \right) \rightarrow \mathcal{N}(0, \sigma&lt;/em&gt;\phi^2)
$$&lt;/p&gt;
&lt;p&gt;$$
\sigma_\phi^2 = \text{Var}[\phi(x_t)] = \int \left( \phi(x) - \int \phi(x&apos;) P(x&apos; | z_{1:t}) dx&apos; \right)^2 P(x | z_{1:t}) dx
$$&lt;/p&gt;
&lt;p&gt;当粒子数量 $N \to \infty$ 时，粒子滤波的蒙特卡洛估计 $\hat{\phi}_N$ &lt;strong&gt;渐近无偏&lt;/strong&gt; ，且服从正态分布误差衰减，说明粒子滤波能逼近真实贝叶斯后验。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;粒子滤波代码讲解&lt;/h1&gt;
&lt;p&gt;粒子滤波是一种基于序贯重要性采样（SIS）的非参数贝叶斯滤波方法，用于对非线性、非高斯系统进行状态估计。它通过一组带权粒子来离散化表示状态分布，并在每个时间步执行 &lt;strong&gt;预测、更新、重采样&lt;/strong&gt; 递推。&lt;/p&gt;
&lt;p&gt;下面的 Python 代码展示了一个粒子滤波的完整实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

class ParticleFilter:
    def __init__(self, N, f, h, Q, R, x0_prior):
        self.N = N
        self.f = f; self.Q = Q
        self.h = h; self.R = R

        # 初始化粒子集合
        self.particles = x0_prior(N)
        self.weights = np.ones(N) / N

    def predict(self):
        for i in range(self.N):
            w = np.random.multivariate_normal(np.zeros(self.Q.shape[0]), self.Q)
            self.particles[i] = self.f(self.particles[i]) + w

    def update(self, z):
        for i in range(self.N):
            v = z - self.h(self.particles[i])
            # 高斯观测似然
            likelihood = np.exp(-0.5 * v.T @ np.linalg.inv(self.R) @ v)
            likelihood /= np.sqrt((2*np.pi)**len(z) * np.linalg.det(self.R))
            self.weights[i] *= likelihood
        # 归一化权重
        self.weights /= np.sum(self.weights)

    def resample(self):
        indices = np.random.choice(self.N, size=self.N, p=self.weights)
        self.particles = self.particles[indices]
        self.weights.fill(1.0 / self.N)

    def estimate(self):
        return np.average(self.particles, weights=self.weights, axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题思考&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;贝叶斯滤波预测步骤用全概率公式展开的目的是什么？&lt;/p&gt;
&lt;p&gt;贝叶斯滤波在预测步骤使用全概率公式展开的目的是为了利用 &lt;strong&gt;系统的状态转移模型&lt;/strong&gt; $P(x_t|x_{t-1})$ 。&lt;/p&gt;
&lt;p&gt;换句话说，我们通过全概率公式引入 $x_{t-1}$ 才能把系统动力学方程（状态转移方程）的条件概率 $P(x_t|x_{t-1})$ 利用上。否则我们没办法从上一步的分布 $P(x_{t-1}|z_{1:t-1})$ 过渡到当前的 $P(x_t|z_{1:t-1})$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在粒子滤波的序贯重要性采样理论中，我们为什么通常会选择状态转移概率作为提议分布？&lt;/p&gt;
&lt;p&gt;在序贯重要性采样中，随着时间的推移，粒子的权重方差会不断增大，导致大多数粒子的权重接近于零，这一现象称为粒子退化问题。选择合适的提议分布可以有效减缓粒子退化问题。理论上可以证明，在最优提议分布 $Q^*(x_t | x_{t-1}, z_t) = P(x_t | x_{t-1}, z_t)$ 下权重方差最小。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体内容可以看下面这个博客&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/676901879&quot;&gt;AMCL深入解析 2/4 - 粒子滤波理论&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在实际应用中，最优提议分布往往难以直接采样，因此通常采用状态转移概率 $P(x_t | x_{t-1})$ 作为近似。粒子滤波正是基于这一思想：在其预测步骤中，粒子通过状态转移模型采样。&lt;/p&gt;
&lt;p&gt;$$
x_t^{(i)} \sim P(x_t | x_{t-1}^{(i)})
$$&lt;/p&gt;
&lt;p&gt;这实际上等价于在序贯重要性采样框架下选用状态转移概率作为提议分布。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献&lt;/h1&gt;
&lt;h2&gt;贝叶斯滤波&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://aandds.com/blog/bayes-filter.html&quot;&gt;Bayes Filter 算法介绍&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/268624245&quot;&gt;从概率到贝叶斯滤波&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/jimmychao1982/article/details/149745121&quot;&gt;贝叶斯滤波器学习笔记&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;卡尔曼滤波&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_37214693/article/details/130927283&quot;&gt;卡尔曼滤波(Kalman Filter)概念介绍及详细公式推导&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/SkyXZ/p/18660856&quot;&gt;【万字长文】让你一文轻松掌握卡尔曼滤波&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/CrescentWind/p/18132934&quot;&gt;Kalman滤波器的原理与实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhengyu.tech/upload/2023/08/Kalman%20Filter.pdf&quot;&gt;卡尔曼滤波（DezemingFamily）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/ishen/p/14987878.html&quot;&gt;从贝叶斯到卡尔曼滤波&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/521538539&quot;&gt;从贝叶斯估计到卡尔曼滤波（详细推导）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;扩展卡尔曼滤波&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/711709657&quot;&gt;扩展卡尔曼滤波(Extended Kalman Filter)原理&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/672506748&quot;&gt;扩展卡尔曼滤波器：含例子及代码&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/550160197&quot;&gt;扩展卡尔曼滤波器实例与推导&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1934805328899323033&quot;&gt;扩展卡尔曼滤波原理与示例&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq570437459/article/details/144704211&quot;&gt;扩展卡尔曼滤波理论推导与实践&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;粒子滤波&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_38410730/article/details/131214213&quot;&gt;【滤波】粒子滤波（PF）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Particle_filter&quot;&gt;【维基百科】Particle Filter&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_44648285/article/details/148074482&quot;&gt;粒子滤波器解读&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.researchgate.net/publication/292354427_Particle_filtering_Theory_approach_and_application_for_multitarget_tracking&quot;&gt;粒子滤波理论、方法及其在多目标跟踪中的应用&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第九节：条件随机场</title><link>https://xingguang641.com/posts/conditional-random-field/conditional-random-field/</link><guid isPermaLink="true">https://xingguang641.com/posts/conditional-random-field/conditional-random-field/</guid><description>介绍机器学习常见的模型</description><pubDate>Wed, 05 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇延续上一篇的 MEMM 进行拓展，CRF 本质上就是为了解决 MEMM 的痛点而产生的。此外，虽然 CRF 来源于 MRF，但理解 CRF 并不需要完全了解 MRF 是什么。因此本篇博客不会介绍 MRF 的相关知识，具体会在后续的概率图算法章节进行介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;随机场基本原理&lt;/h1&gt;
&lt;p&gt;由于 MEMM 存在标注偏置问题，为此 Lafferty J、 Mccallum A 和 Pereira F C N 三人在 2001 年提出了一种 &lt;strong&gt;线性链条件随机场&lt;/strong&gt;（Conditional Random Fields，简称 CRF）模型，该模型拥有 MEMM 的所有优点，同时还 &lt;strong&gt;不存在标注偏置问题&lt;/strong&gt; 。条件随机场的一般定义如下：&lt;/p&gt;
&lt;p&gt;设 $X$ 与 $Y$ 是随机变量， $P(Y|X)$ 是在给定 $X$ 的条件下 $Y$ 的条件概率分布。若随机变量 $Y$ 构成一个由无向图 $G = (V, E)$ 表示的马尔可夫随机场，即 $P(Y_v | X, Y_w, w \neq v) = P(Y_v | X, Y_w, w \sim v)$ 对任意结点 $v$ 成立，则称条件概率分布 $P(Y|X)$ 为条件随机场。&lt;/p&gt;
&lt;p&gt;式中 $w \sim v$ 表示在图 $G = (V, E)$ 中与结点 $v$ 有边连接的所有结点 $w$ ，$w \neq v$ 表示结点 $v$ 以外的所有结点， $Y_v$ 、$Y_w$ 为结点 $v$ 、 $w$ 对应的随机变量。&lt;/p&gt;
&lt;p&gt;在条件随机场的一般定义中并没有要求 $X$ 和 $Y$ 具有相同的图结构，但是实际运用中一般假设 $X$ 和 $Y$ 具有相同的图结构，并且线性链条件随机场也同样作此假设。线性链条件随机场的定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cconditional-random-field%5C%E6%9D%A1%E4%BB%B6%E9%9A%8F%E6%9C%BA%E5%9C%BA1.png&quot; alt=&quot;条件随机场图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;基本形式定义&lt;/h2&gt;
&lt;p&gt;设 $X = (x_1, x_2, \ldots, x_n)$ 和 $Y = (y_1, y_2, \ldots, y_n)$ 均为线性链表示的随机变量序列，若在给定随机量序列 $X$ 的条件下，随机变量序列 $Y$ 的条件概率分布 $P(Y|X)$ 构成条件随机场，即满足马尔可夫性（在 $i = 1$ 和 $n$ 时只考虑单边）：&lt;/p&gt;
&lt;p&gt;$$
P(y_i | X, y_1, \cdots, y_{i-1}, y_{i+1}, \cdots, y_n) = P(y_i | X, y_{i-1}, y_{i+1}) \quad i = 1, 2, \ldots, n
$$&lt;/p&gt;
&lt;p&gt;则称 $P(Y|X)$ 为线性链条件随机场。线性链条件随机场通常用来对序列标注问题进行建模，在序列标注问题中， $X$ 可以看作 &lt;strong&gt;观测序列&lt;/strong&gt; ，$Y$ 可以看做对应的 &lt;strong&gt;状态序列&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;根据线性链条件随机场的定义可知，此时由 $Y$ 构成的马尔可夫随机场的最大团为相邻两个结点的集合，那么由 Hammersley-Clifford 定理可知，线性链条件随机场 $P(Y|X)$ 的表达式可以写为如下形式：&lt;/p&gt;
&lt;p&gt;$$
P(Y|X) = \frac{1}{Z(X)} \exp \left( \sum_{i,k} \lambda_k t_k(y_{i-1}, y_i, X, i) + \sum_{i,l} \mu_i s_l(y_i, X, i) \right)
$$&lt;/p&gt;
&lt;p&gt;$$
Z(X) = \sum_Y \exp \left( \sum_{i,k} \lambda_k t_k(y_{i-1}, y_i, X, i) + \sum_{i,l} \mu_l s_l(y_i, X, i) \right)
$$&lt;/p&gt;
&lt;p&gt;其中 $Z(X)$ 是 &lt;strong&gt;规范化因子&lt;/strong&gt; ，求和是在所有可能的输出序列上进行的。 $t_k$ 是定义在边上的特征函数，称为 &lt;strong&gt;转移特征&lt;/strong&gt; ，依赖于当前和前一个位置； $s_l$ 是定义在结点上的特征函数，称为 &lt;strong&gt;状态特征&lt;/strong&gt; ，依赖于当前位置。 $t_k$ 和 $s_l$ 都依赖于位置，是 &lt;strong&gt;局部特征函数&lt;/strong&gt; 。线性链条件随机场完全由特征函数 $t_k$ 、$s_l$ 和对应的权值 $\lambda_k$ 、$\mu_i$ 确定（通常特征函数是事先人为设定好的超参数，而权值则是通过学习得到）。&lt;/p&gt;
&lt;p&gt;观察上式易知：线性链条件随机场为判别式模型，同时也实现了用特征对观测序列参数化，而且状态转移概率采用的是全局归一化来计算。所以线性链条件随机场拥有 MEMM 的所有优点，而且还不存在标注偏置问题。&lt;/p&gt;
&lt;h2&gt;向量形式定义&lt;/h2&gt;
&lt;p&gt;根据特征函数的性质可知，状态特征函数 $s_l$ 可以看做是只提取当前位置特征的转移特征函数，即 $s_l(y_i, X, i) = s_l(y_{i - 1}, X, i)$ 。因此 $P(X|Y)$ 表达式中的转移特征和状态特征及其权值可以用统一的符号表示。不妨设有 $K_1$ 个转移特征， $K_2$ 个状态特征，则 $K = K_1 + K_2$ 。若序列长度为 $n$ ，则 $P(Y|X)$ 可以简写为：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(Y|X) &amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_{i,k} \lambda_k t_k(y_{i-1}, y_i, X, i) + \sum_{i,l} \mu_i s_l(y_i, X, i) \right) \
&amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_i \sum_{k=1}^{K_1} \lambda_k t_k(y_{i-1}, y_i, X, i) + \sum_i \sum_{l=1}^{K_2} \mu_i s_l(y_i, X, i) \right) \
&amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_i \sum_{k=1}^{K_1} \lambda_k t_k(y_{i-1}, y_i, X, i) + \sum_i \sum_{l=1}^{K_2} \mu_i s_l(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_i \sum_{k=1}^{K_1+K_2} w_k f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_i \sum_{k=1}^{K} w_k f_k(y_{i-1}, y_i, X, i) \right)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;不妨设：&lt;/p&gt;
&lt;p&gt;$$
f_k(Y, X) = \sum_i f_k(y_{i-1}, y_i, X, i) \quad k = 1, 2, \ldots, K
$$&lt;/p&gt;
&lt;p&gt;$$
F(Y, X) = \Big(f_1(Y, X), f_2(Y, X), \ldots, f_K(Y, X)\Big) \in \mathbb{R}^{K \times 1}
$$&lt;/p&gt;
&lt;p&gt;$$
w = (w_1, w_2, \ldots, w_k) \in \mathbb{R}^{K \times 1}
$$&lt;/p&gt;
&lt;p&gt;那么 $P(Y|X)$ 可以进一步简写为如下向量化形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(Y|X) &amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_i \sum_{k=1}^K w_k f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \frac{1}{Z(X)} \exp \left( \sum_{k=1}^K w_k \sum_i f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \frac{\exp(w^{\rm T} F(Y, X))}{Z(X)}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } Z(X) = \sum_Y \exp(w^{\rm T} F(Y, X))
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;随机场实现难点&lt;/h1&gt;
&lt;p&gt;在条件随机场（CRF）中，构建模型同样要围绕三类基本问题展开：如何在给定观测下计算条件概率与相关边缘分布、如何基于标注数据学习模型参数、以及如何在已学得模型下推断最可能的状态序列。与 HMM 和 MEMM 不同，CRF 是判别式的全局序列模型：它直接建模 $P(Y |d X)$ 并通过一个全局归一化因子（配分函数）联结全序列的特征，从而避免了局部归一化带来的标注偏差问题，这一点在实际序列标注任务中非常重要。&lt;/p&gt;
&lt;p&gt;首先是 &lt;strong&gt;计算问题&lt;/strong&gt; 。当模型参数 $w_k\ (k=1,2,\dots,K)$ 、观测序列 $X=(x_1,x_2,\dots,x_n)$ 与状态序列 $Y=(y_1,y_2,\dots,y_n)$ 已知时，我们需要计算条件概率 $P(Y | X)$ 以及与训练和推断相关的各种边缘量，例如单点边缘 $P(y_i | X)$ 、相邻对的联合边缘 $P(y_{i-1},y_i | X)$ ，以及这些分布下的数学期望。由于 CRF 的概率由未归一化的能量函数通过配分函数 $Z(X)$ 做整体归一化得到，直接枚举是不现实的，因此常用的做法是借助动态规划（前向-后向算法、或在图上做精确或近似的 Belief Propagation）来高效计算这些边缘分布和期望值，这些结果既是评估模型的基础，也是参数学习与不确定性量化所必需的中间量。&lt;/p&gt;
&lt;p&gt;其次是 &lt;strong&gt;学习问题&lt;/strong&gt; 。在给定配对标注数据 $(X,Y)$ 的条件下，CRF 的参数学习通常采用条件对数似然最大化的策略：我们以训练集中每个样本的对数条件概率之和作为目标（常加上正则化项以抑制过拟合），并通过计算目标关于参数的梯度来指导优化。这里的梯度包含两部分：一是数据项（特征在真实标注下的计数或期望），二是模型项（在当前参数下特征的期望），而后者又需借助前向-后向或相应的边缘计算来获得。因此，CRF 的训练既是一个数值优化问题，也与高效的推断（用于计算期望）紧密耦合。常见的数值优化器包括 L-BFGS、共轭梯度或带动量的梯度方法；在大规模数据上，也可以采用分批或随机优化策略并结合正则化（例如 $L_2$ 正则）以提高泛化能力。&lt;/p&gt;
&lt;p&gt;最后是 &lt;strong&gt;预测问题&lt;/strong&gt; 。在模型参数确定且给定观测序列 $X$ 的条件下，我们希望找到能使条件概率 $P(Y | X)$ 最大的状态序列 $Y$ 。由于 CRF 的概率是全局定义的，但通常仍可利用动态规划来高效求解最优序列：在链式 CRF（线性链 CRF）中，Viterbi 算法的思想可以直接应用，通过对数空间的递推找到最大得分路径；对于更复杂的图结构，可能需要采用图切分、束搜索或近似推断方法来在可接受的计算成本内找到高概率的标注路径。需要注意的是，CRF 的全局建模使得解码结果更关注序列整体一致性，这往往比逐步局部判别的模型（如 MEMM）给出更鲁棒、连贯的标签序列。&lt;/p&gt;
&lt;p&gt;综上所述，这三个问题分别对应 CRF 的三类核心任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;条件概率与边缘计算&lt;/strong&gt;：计算条件概率 $P(Y | X)$ 、$P(y_i | X)$ 、$P(y_{i-1},y_i | X)$ 及其期望&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数学习&lt;/strong&gt;：最大化条件对数似然，利用边缘期望构造梯度，借助随机优化等数值方法求解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列解码&lt;/strong&gt;：在给定 $X$ 与参数下，用 Viterbi 或相应的动态规划/搜索方法找到使 $P(Y | X)$ 最大的状态序列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在理解了这些问题及其相互关系后，我们可以进一步展开每一部分的具体算法推导。&lt;/p&gt;
&lt;h2&gt;计算问题&lt;/h2&gt;
&lt;h3&gt;计算条件概率&lt;/h3&gt;
&lt;p&gt;由 $P(Y|X)$ 的表达式可知，要想计算出条件概率 $P(Y|X)$ 则需要计算出给定状态序列 $Y$ 的非规范化概率 $exp(w^{\rm T}F(Y, X))$ 和规范化因子 $Z(X)$ 。由于在已知观测序列 $X$ 和模型参数 $w_k(k = 1, 2, \ldots, K)$ 的条件下，只要知道状态的取值范围，无论对应状态序列 $Y$ 是否已知，均能求出规范化因子 $Z(X)$ 。&lt;/p&gt;
&lt;p&gt;所以下面考虑对 $Z(X)$ 和 $exp(w^{\rm T}F(Y, X))$ 分别进行求解。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先考虑求解 $Z(X)$&lt;/p&gt;
&lt;p&gt;设状态的取值范围为 $Q = { q_1, q_2, \ldots, q_m }$ ，将所有状态序列前后都各填充一个 $y_0 = start$ 和 $y_{n+1} = stop$ 。对观测序列 $X$ 的每一个位置 $i = 1, 2, \ldots, n+1$ 来说， $y_{i-1}$ 和 $y_i$ 都有 $m$ 种可能的取值，因此，对于每一个位置来说都可以定义一个 $m \times m$ 的 &lt;strong&gt;转移势矩阵&lt;/strong&gt; ：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{M}&lt;em&gt;i(X) = \Big[ M_i(y&lt;/em&gt;{i-1}, y_i | X) \Big] = \begin{bmatrix}
M_1(q_1, q_1 | X) &amp;amp; M_1(q_1, q_2 | X) &amp;amp; \ldots &amp;amp; M_1(q_1, q_m | X) \
M_1(q_2, q_1 | X) &amp;amp; M_1(q_2, q_2 | X) &amp;amp; \ldots &amp;amp; M_1(q_2, q_m | X) \
\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots \
M_1(q_m, q_1 | X) &amp;amp; M_1(q_m, q_2 | X) &amp;amp; \ldots &amp;amp; M_1(q_m, q_m | X)
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } M_i(y_{i-1}, y_i | X) = \exp \left( \sum_{k=1}^{K} w_k f_k(y_{i-1}, y_i, X, i) \right)
$$&lt;/p&gt;
&lt;p&gt;特别地，对于起始位置 $i = 1$ 和结束位置 $i = n + 1$ 的矩阵定义为（确保初始和结尾位置状态确定）：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{M}_1(X) =
\begin{bmatrix}
M_1(start, q_1 | X) &amp;amp; M_1(start, q_2 | X) &amp;amp; \ldots &amp;amp; M_1(start, q_m | X) \
0 &amp;amp; 0 &amp;amp; \ldots &amp;amp; 0 \
\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots \
0 &amp;amp; 0 &amp;amp; \ldots &amp;amp; 0
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
\mathbf{M}&lt;em&gt;{n+1}(X) =
\begin{bmatrix}
M&lt;/em&gt;{n+1}(q_1, stop | X) = 1 &amp;amp; 0 &amp;amp; \ldots &amp;amp; 0 \
M_{n+1}(q_2, stop | X) = 1 &amp;amp; 0 &amp;amp; \ldots &amp;amp; 0 \
\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots \
M_{n+1}(q_m, stop | X) = 1 &amp;amp; 0 &amp;amp; \ldots &amp;amp; 0
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;此时 $Z(X)$ 为 $\mathbf{M}_i(X)$ 这 $n+1$ 个矩阵的乘积的第 1 行第 1 列元素：&lt;/p&gt;
&lt;p&gt;$$
Z(X) = \left[ \prod_{i=1}^{n+1} \mathbf{M}&lt;em&gt;i(X) \right]&lt;/em&gt;{(1,1)}
$$&lt;/p&gt;
&lt;p&gt;根据矩阵相乘的性质，所有 $\mathbf{M}_i(X)$ 相乘的最终结果就是初始位置的状态到结尾位置的状态的所有路径的权重之积再求和。因此 $Z(X)$ 的表达式为 $n+1$ 个矩阵的乘积的第 1 行第 1 列元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后考虑 $exp(w^{\rm T}F(Y, X))$&lt;/p&gt;
&lt;p&gt;在对应状态序列 $Y$ 也已知的条件下，则可以通过 $M_i(X)$ 这 $n+1$ 个矩阵的适当元素的乘积来表示：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\exp(w^{\rm T} F(Y, X)) &amp;amp;= \exp \left( \sum_{k=1}^K w_k \sum_{i=1}^{n+1} f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \exp \left( \sum_{i=1}^{n+1} \sum_{k=1}^K w_k f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \prod_{i=1}^{n+1} \exp \left( \sum_{k=1}^K w_k f_k(y_{i-1}, y_i, X, i) \right) \
&amp;amp;= \prod_{i=1}^{n+1} M_i(y_{i-1}, y_i | X)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;首先我们要知道的是，非规范化概率在概率图中表示的是一个具体的路径。所以 $exp(w^{\rm T}F(Y, X))$ 的表达式自然是上面这个公式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;定义前/后向向量&lt;/h3&gt;
&lt;p&gt;我们接着来定义一下前/后向向量（与 HMM 的前/后向概率相似，这里给出的是向量形式）&lt;/p&gt;
&lt;p&gt;对每个位置 $i = 1, 2, \ldots, n+1$ 定义前向向量 $\boldsymbol{\alpha}_i(X) \in \mathbb{R}^{m \times 1}$ ：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{\alpha}_0(X) = \begin{bmatrix} 1 \ 0 \ \vdots \ 0 \end{bmatrix} \quad \boldsymbol{\alpha}_i(X) = \begin{bmatrix} \alpha_i(y_i = q_1 | X) \ \alpha_i(y_i = q_2 | X) \ \vdots \ \alpha_i(y_i = q_m | X) \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;其中 $\alpha_i(y_i = q_j|X)(j = 1, 2, \ldots, m)$ 表示在位置 $i$ 的状态是 $q_j$ 并且从 1 到 $i$ 的状态序列的非规范化概率。根据前向向量的定义易得递推公式：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{\alpha}&lt;em&gt;i(X)^{\rm T} = \boldsymbol{\alpha}&lt;/em&gt;{i-1}(X)^{\rm T} \Big[ M_i(y_{i-1}, y_i | X) \Big] = \boldsymbol{\alpha}_{i-1}(X)^{\rm T} \mathbf{M}_i(X)
$$&lt;/p&gt;
&lt;p&gt;同理，对每个位置 $i = 1, 2, \ldots, n+1$ 定义后向向量 $\boldsymbol{\beta}_i(x) \in \mathbb{R}^{m \times 1}$ ：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{\beta}&lt;em&gt;i(X) = \begin{bmatrix} \beta_i(y_i = q_1 | X) \ \beta_i(y_i = q_2 | X) \ \vdots \ \beta_i(y_i = q_m | X) \end{bmatrix} \quad \boldsymbol{\beta}&lt;/em&gt;{n+1}(X) = \begin{bmatrix} 1 \ 0 \ \vdots \ 0 \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;其中 $\beta_i(y_i = q_j|X)(j = 1, 2, \ldots, m)$ 表示在位置 $i$ 的状态是 $q_j$ 并且从 $i+1$ 到最后的状态序列的非规范化概率。根据后向向量的定义易得递推公式：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{\beta}&lt;em&gt;i(X) = \Big[ M&lt;/em&gt;{i+1}(y_i, y_{i+1} | X) \Big] \boldsymbol{\beta}_{i+1}(X) = \mathbf{M}&lt;em&gt;i(X) \boldsymbol{\beta}&lt;/em&gt;{i+1}(X)
$$&lt;/p&gt;
&lt;p&gt;定义完前向向量和后向向量，接下来便可以很容易地计算出在位置 $i$ 的状态是 $q_j$ 的条件概率和在位置 $i-1$ 是状态 $q_j$ 且在位置 $i$ 是状态 $q_k$ 的条件概率：&lt;/p&gt;
&lt;p&gt;$$
P(y_i | X) = \frac{\alpha_i(y_i = q_j | X) \beta_i(y_i = q_j | X)}{Z(X)}
$$&lt;/p&gt;
&lt;p&gt;$$
P(y_{i-1}, y_i | X) = \frac{\alpha_{i-1}(y_{i-1} = q_j | X) M_i(q_j, q_k | X) \beta_i(y_i = q_k | X)}{Z(X)}
$$&lt;/p&gt;
&lt;p&gt;其中 $Z(X)$ 为归一化因子：&lt;/p&gt;
&lt;p&gt;$$
Z(X) = \boldsymbol{\alpha}&lt;em&gt;n(X)^{\rm T} \mathbf{I} = \boldsymbol{\alpha}&lt;/em&gt;{n+1}(X)^{\rm T} \mathbf{I} = \mathbf{I}^{\rm T} \boldsymbol{\beta}_0(X) \quad \mathbf{I} = (1, \ldots, 1) \in \mathbb{R}^{m \times 1}
$$&lt;/p&gt;
&lt;h3&gt;计算期望值&lt;/h3&gt;
&lt;p&gt;利用前面定义的前向向量和后向向量，我们可以轻松地计算出特征函数关于联合分布 $P(X, Y)$ 和条件分布 $P(Y|X)$ 的数学期望。考虑特征函数：&lt;/p&gt;
&lt;p&gt;$$
f_k(Y, X) = \sum_{i=1}^{n}f_k(y_{i-1}, y_i, X, i)
$$&lt;/p&gt;
&lt;p&gt;关于条件分布 $P(Y|X)$ 的数学期望是：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\mathbb{E}&lt;em&gt;{P(Y|X)} \Big[ f_k(Y, X) \Big] &amp;amp;= \sum_Y \left[ P(Y|X) \sum&lt;/em&gt;{i=1}^{n+1} f_k(y_{i-1}, y_i, X, i) \right] = \sum_{i=1}^{n+1} \sum_Y P(Y|X) f_k(y_{i-1}, y_i, X, i) \
&amp;amp;= \sum_{i=1}^{n+1} \sum_{j=1}^m \sum_{k=1}^m \Big[ f_k(y_{i-1} = q_j, y_i = q_k, X, i) P(y_{i-1} = q_j, y_i = q_k | X) \Big]
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;其中 $Z(X)$ 为归一化因子。&lt;/p&gt;
&lt;p&gt;关于联合分布 $P(X, Y)$ 的数学期望是：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\mathbb{E}&lt;em&gt;{P(X,Y)} \Big[ f_k(Y, X) \Big] &amp;amp;= \sum&lt;/em&gt;{X,Y} P(X,Y) f_k(Y, X) = \sum_{X,Y} \bar{P}(X) P(Y|X) f_k(Y, X) \
&amp;amp;= \sum_X \bar{P}(X) \sum_Y P(Y|X) f_k(Y, X) = \sum_X \bar{P}(X) \mathbb{E}_{P(Y|X)} \Big[ f_k(Y, X) \Big]
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{P}(X)$ 为经验分布。&lt;/p&gt;
&lt;p&gt;综上所述，对于在给定模型参数 $w_k(k = 1, 2, \ldots, K)$ 、观测序列 $X = (x_1, x_2, \ldots, x_n)$ 和状态序列 $Y = (y_1, y_2, \ldots, y_n)$ 的条件下，只需前向扫描计算和后向扫描计算一次 $\boldsymbol{\alpha}_i(X)$ 和 $\boldsymbol{\beta}&lt;em&gt;i(X)$ ，规范化因子 $Z(X)$ 和条件概率 $P(y_i|X)$ 、 $P(y&lt;/em&gt;{i-1}, y_i|X)$ 以及一些数学期望都可以被计算出来。&lt;/p&gt;
&lt;h2&gt;学习问题&lt;/h2&gt;
&lt;p&gt;在给定观测序列 $X = (x_1, x_2, \ldots, x_n)$ 和对应状态序列 $Y = (y_1, y_2, \ldots, y_n)$ 的条件下，可以通过极大似然估计法来估计模型的参数。由于线性链条件随机场类似于最大熵模型，所以用于求解最大熵模型参数的 GIS、IIS、梯度下降、牛顿法和拟牛顿法均可用于线性链条件随机场。&lt;/p&gt;
&lt;h2&gt;预测问题&lt;/h2&gt;
&lt;p&gt;线性链条件随机场的预测问题是在给定模型参数 $w_k(k = 1, 2, \ldots, K)$ 、观测序列 $X = (x_1, x_2, \ldots, x_n)$ 的条件下，求条件概率最大的状态序列 $Y^* = (y_1^&lt;em&gt;, y_2^&lt;/em&gt;, \ldots, y_n^*)$ ，即对观测序列进行标注。线性链条件随机场解决预测问题所采用的算法和 HMM 和 MEMM 一样，采用的都是经典的 Viterbi 算法。具体如下：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
Y^* &amp;amp;= \arg\max_Y P(Y|X) \
&amp;amp;= \arg\max_Y \frac{\exp(\boldsymbol{w}^T F(Y, X))}{Z(X)} \
&amp;amp;= \arg\max_Y \exp(\boldsymbol{w}^T F(Y, X)) \
&amp;amp;= \arg\max_Y (\boldsymbol{w}^T F(Y, X))
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;于是，线性链条件随机场的预测问题转化为了求非规范化概率最大的最优路径问题，其中路径表示的是状态序列。为了求解最优路径，将上式作如下恒等变形：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
Y^* &amp;amp;= \arg\max_Y  \boldsymbol{w}^T F(Y, X) \
&amp;amp;= \arg\max_Y \sum_{k=1}^K w_k \sum_i f_k(y_{i-1}, y_i, X, i) \
&amp;amp;= \arg\max_Y \sum_i \sum_{k=1}^K w_k f_k(y_{i-1}, y_i, X, i) \
&amp;amp;= \arg\max_Y \sum_i \boldsymbol{w}^T F_i(y_{i-1}, y_i, X)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } F_i(y_{i-1}, y_i, X) = \Big(f_1(y_{i-1}, y_i, X, i), f_2(y_{i-1}, y_i, X, i), \ldots, f_K(y_{i-1}, y_i, X, i)\Big) \in \mathbb{R}^{K \times 1}
$$&lt;/p&gt;
&lt;p&gt;首先求出位置 1 的各个标记 $y_1 = q_1, q_2, \ldots, q_m$ 的非规范化概率：&lt;/p&gt;
&lt;p&gt;$$
\delta_1(j) = w^{\rm T} F_1(y_0 = start, y_1 = q_j, X) \quad j = 1, 2, \ldots, m
$$&lt;/p&gt;
&lt;p&gt;接着由递推公式，求出到位置 $i$ 的各个标记 $l = 1, 2, \ldots, m$ 的非规范化概率的最大值，同时记录非规范化概率最大值的路径：&lt;/p&gt;
&lt;p&gt;$$
\delta_i(l) = \max_{1 \leq j \leq m} \left{ \delta_{i-1}(j) + w^{\rm T} F_i(y_{i-1} = q_j, y_i = q_l, X) \right} \quad l = 1, 2, \ldots, m
$$
f
$$
\Psi_i(l) = \arg\max_{1 \leq j \leq m} \left{ \delta_{i-1}(j) + w^{\rm T} F_i(y_{i-1} = q_j, y_i = q_l, X) \right} \quad l = 1, 2, \ldots, m
$$&lt;/p&gt;
&lt;p&gt;直到 $i = n$ 时终止，此时求得的非规范化概率的最大值为：&lt;/p&gt;
&lt;p&gt;$$
\max_Y w^{\rm T} F(Y, X) = \max_{1 \leq j \leq m} \delta_n(j)
$$&lt;/p&gt;
&lt;p&gt;最优路径的终点为：&lt;/p&gt;
&lt;p&gt;$$
y_n^* = \arg\max_{1 \leq j \leq m} \delta_n(j)
$$&lt;/p&gt;
&lt;p&gt;接着从最优路径的终点回溯即可求得最优路径。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;随机场代码讲解&lt;/h1&gt;
&lt;p&gt;下面给出 CRF 的代码，具体原理自行观看上述证明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
from collections import defaultdict
train_data = [
    ([&apos;The&apos;, &apos;capital&apos;, &apos;of&apos;, &apos;France&apos;], [&apos;B-LOC&apos;, &apos;O&apos;, &apos;O&apos;, &apos;B-LOC&apos;]),
    ([&apos;The&apos;, &apos;president&apos;, &apos;of&apos;, &apos;USA&apos;], [&apos;B-LOC&apos;, &apos;O&apos;, &apos;O&apos;, &apos;B-LOC&apos;]),
    ([&apos;I&apos;, &apos;love&apos;, &apos;Paris&apos;], [&apos;O&apos;, &apos;O&apos;, &apos;B-LOC&apos;])
]


# 特征提取函数
def extract_features(sentence, index):
    features = {
        &apos;word&apos;: sentence[index],
        &apos;is_capitalized&apos;: sentence[index][0].isupper(),
        &apos;is_digit&apos;: sentence[index].isdigit(),
        &apos;word[-3:]&apos;: sentence[index][-3:],
    }
    # 上一个词
    if index &amp;gt; 0:
        features[&apos;prev_word&apos;] = sentence[index - 1]
    # 下一个词
    if index &amp;lt; len(sentence) - 1:
        features[&apos;next_word&apos;] = sentence[index + 1]
    return features

class CRF:
    def __init__(self):
        self.weights = defaultdict(float)
        self.transition_weights = defaultdict(float)
    
    def _features_to_key(self, features):
        return tuple(sorted(features.items()))
    
    def _get_feature_score(self, features):
        feature_key = self._features_to_key(features)
        return self.weights.get(feature_key, 0)

    def train(self, data, epochs=10, learning_rate=0.1):
        for epoch in range(epochs):
            for sentence, labels in data:
                # 计算每个单词的特征和标签得分
                for i in range(len(sentence)):
                    features = extract_features(sentence, i)
                    feature_score = self._get_feature_score(features)
                    feature_key = self._features_to_key(features)
                    # 计算特征的得分并更新权重
                    self.weights[feature_key] += learning_rate
                # 更新转移权重
                for i in range(1, len(labels)):
                    transition_key = (labels[i-1], labels[i])
                    self.transition_weights[transition_key] += learning_rate
            # 打印当前轮次的训练进度
            print(f&apos;Epoch {epoch + 1} complete.&apos;)

    def viterbi_decode(self, sentence):
        n = len(sentence)
        dp = np.zeros((n, len(self.transition_weights)))
        backpointer = np.zeros((n, len(self.transition_weights)), dtype=int)
        # 初始化第一列：根据特征和初始转移权重
        for i in range(len(self.transition_weights)):
            features = extract_features(sentence, 0)
            dp[0][i] = self._get_feature_score(features) + self.transition_weights.get((&apos;&amp;lt;START&amp;gt;&apos;, i), 0)
        # 动态规划：计算每个位置的最优标签路径
        for i in range(1, n):
            for j in range(len(self.transition_weights)):
                max_score = -float(&apos;inf&apos;)
                max_index = -1
                for k in range(len(self.transition_weights)):
                    features = extract_features(sentence, i)
                    score = dp[i-1][k] + self.transition_weights.get((k, j), 0) + self._get_feature_score(features)
                    if score &amp;gt; max_score:
                        max_score = score
                        max_index = k
                dp[i][j] = max_score
                backpointer[i][j] = max_index
        # 回溯：找到最优标签序列
        best_path = []
        best_state = np.argmax(dp[n-1])
        best_path.append(best_state)
        for i in range(n-2, -1, -1):
            best_state = backpointer[i+1][best_state]
            best_path.insert(0, best_state)
        return best_path

    def predict(self, sentence):
        predictions = []
        for i in range(len(sentence)):
            features = extract_features(sentence, i)
            score = self._get_feature_score(features)
            predictions.append(score)
        return predictions


# 执行代码
if __name__ == &quot;__main__&quot;:
    crf_model = CRF()
    crf_model.train(train_data, epochs=10)

    test_sentence = [&apos;I&apos;, &apos;love&apos;, &apos;Paris&apos;]
    predictions = crf_model.predict(test_sentence)
    print(f&apos;Predictions: {predictions}&apos;)

    # 使用 Viterbi 解码获取标签序列
    best_path = crf_model.viterbi_decode(test_sentence)
    print(f&apos;Predicted path (labels): {best_path}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 学习问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#%E5%AD%A6%E4%B9%A0%E9%97%AE%E9%A2%98&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def _features_to_key(self, features):
    return tuple(sorted(features.items()))

def _get_feature_score(self, features):
    feature_key = self._features_to_key(features)
    return self.weights.get(feature_key, 0)

def train(self, data, epochs=10, learning_rate=0.1):
    for epoch in range(epochs):
        for sentence, labels in data:
            # 计算每个单词的特征和标签得分
            for i in range(len(sentence)):
                features = extract_features(sentence, i)
                feature_score = self._get_feature_score(features)
                feature_key = self._features_to_key(features)
                # 计算特征的得分并更新权重
                self.weights[feature_key] += learning_rate
            # 更新转移权重
            for i in range(1, len(labels)):
                transition_key = (labels[i-1], labels[i])
                self.transition_weights[transition_key] += learning_rate
        # 打印当前轮次的训练进度
        print(f&apos;Epoch {epoch + 1} complete.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 预测问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#%E9%A2%84%E6%B5%8B%E9%97%AE%E9%A2%98&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def viterbi_decode(self, sentence):
    n = len(sentence)
    dp = np.zeros((n, len(self.transition_weights)))
    backpointer = np.zeros((n, len(self.transition_weights)), dtype=int)
    # 初始化第一列：根据特征和初始转移权重
    for i in range(len(self.transition_weights)):
        features = extract_features(sentence, 0)
        dp[0][i] = self._get_feature_score(features) + self.transition_weights.get((&apos;&amp;lt;START&amp;gt;&apos;, i), 0)
    # 动态规划：计算每个位置的最优标签路径
    for i in range(1, n):
        for j in range(len(self.transition_weights)):
            max_score = -float(&apos;inf&apos;)
            max_index = -1
            for k in range(len(self.transition_weights)):
                features = extract_features(sentence, i)
                score = dp[i-1][k] + self.transition_weights.get((k, j), 0) + self._get_feature_score(features)
                if score &amp;gt; max_score:
                    max_score = score
                    max_index = k
            dp[i][j] = max_score
            backpointer[i][j] = max_index
    # 回溯：找到最优标签序列
    best_path = []
    best_state = np.argmax(dp[n-1])
    best_path.append(best_state)
    for i in range(n-2, -1, -1):
        best_state = backpointer[i+1][best_state]
        best_path.insert(0, best_state)
    return best_path
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/hei653779919/article/details/104227606&quot;&gt;机器学习——条件随机场(CRF)原理&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Determined22/p/6915730.html&quot;&gt;NLP —— 图模型（二）条件随机场&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_43988131/article/details/148777675&quot;&gt;【深度学习】条件随机场（CRF）深度解析&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://starmaye.github.io/NLP/%E6%9D%A1%E4%BB%B6%E9%9A%8F%E6%9C%BA%E5%9C%BA%E4%B9%8B%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E4%B8%8E%E6%A8%A1%E5%9E%8B/&quot;&gt;条件随机场之基本概念与模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sm1les.com/2019/08/27/conditional-random-fields/&quot;&gt;条件随机场（CRF）及其三个基本问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第八节：最大熵马尔可夫模型</title><link>https://xingguang641.com/posts/maximum-entropy-markov/maximum-entropy-markov/</link><guid isPermaLink="true">https://xingguang641.com/posts/maximum-entropy-markov/maximum-entropy-markov/</guid><description>介绍机器学习常见的模型</description><pubDate>Sun, 02 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本篇延续上一篇的 HMM 进行拓展，MEMM 本质上就是为了解决 HMM 的痛点而产生的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;最大熵马尔可夫模型基本原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最大熵马尔可夫模型&lt;/strong&gt;（Maximum-entropy Markov model，简称 MEMM）由 Andrew McCallum、Dayne Freitag 和 Fernando Pereira 三人于 2000 年提出。它结合了隐马尔可夫模型（HMM）和最大熵模型（MEM），被广泛应用于处理序列标注问题。文献认为在 HMM 中主要存在以下两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;无法用特征对观测序列参数化：在很多序列标注任务中，尤其当不能枚举所有观测序列时，通常需要用大量的特征来刻画观测序列。比如在文本中识别一个未见过的公司名字时，通常需要用到很多特征信息，如大写字母、结尾词、词性、格式、在文本中的位置等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;判别式模型比生成式模型更适合处理序列标注问题：HMM 多被用在处理序列标注问题，序列标注问题的目标是求出状态相对于观测的条件概率 $P(state|observation)$ ，而 HMM 是对状态和观测的联合概率 $P(state, observation)$ 进行建模的生成式模型，相对于直接对 $P(state|observation)$ 进行建模的判别式模型来说，显然判别式模型更适合处理虚了标注问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MEMM 的具体定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cmaximum-entropy-markov%5C%E6%9C%80%E5%A4%A7%E7%86%B5%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B1.png&quot; alt=&quot;最大熵马尔可夫模型图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;若想知道什么是最大熵马尔可夫模型，我们就必须先弄清楚什么是最大熵模型。&lt;/p&gt;
&lt;h2&gt;理论基础&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;最大熵模型&lt;/strong&gt;（Maximum Entropy Model，简称 MaxEnt）是一种经典的分类算法，它与逻辑回归同属于 &lt;strong&gt;对数线性分类模型&lt;/strong&gt; 。从算法实现上看，最大熵模型通过优化损失函数来训练参数，其优化过程通常采用与支持向量机类似的凸优化技术，因此可以保证全局最优解的可达性。&lt;/p&gt;
&lt;p&gt;最大熵模型的理论基础是 &lt;strong&gt;最大熵原理&lt;/strong&gt;：在已知条件约束下，选择熵最大的概率分布作为模型的输出分布。这意味着模型不会对数据引入额外的假设或偏向，而是以最 “均匀” 、最 “不确定” 的方式去刻画已知信息。这种思想不仅可以有效避免过拟合，还能在数据稀疏或信息不完全的情况下保持模型的稳健性。&lt;/p&gt;
&lt;p&gt;简而言之，最大熵模型的核心目标是：在保证符合观测约束的前提下，使模型输出的概率分布尽可能接近最大不确定性，从而实现对数据的最合理建模。&lt;/p&gt;
&lt;h3&gt;最大熵原理&lt;/h3&gt;
&lt;p&gt;最大熵原理是概率模型学习的一个准则。最大熵原理认为：学习概率模型时，在所有可能的概率模型/分布中，&lt;strong&gt;熵最大的模型是最好的模型&lt;/strong&gt; 。通常用约束条件来确定概率模型的集合，所以，最大熵原理也可以表述为在满足约束条件的模型集合中选取熵最大的模型。&lt;/p&gt;
&lt;p&gt;假设离散随机变量 $X$ 的概率分布是 $P(X)$ ，则其熵是：&lt;/p&gt;
&lt;p&gt;$$
H(P) = - \sum_x P(x) \log P(x)
$$&lt;/p&gt;
&lt;p&gt;而熵满足以下不等式：&lt;/p&gt;
&lt;p&gt;$$
0 \leq H(P) \leq \log |X|
$$&lt;/p&gt;
&lt;p&gt;其中 $|X|$ 是 $X$ 的取值个数，当且仅当 $X$ 的分布是均匀分布时右边的等号成立。也就是说，当 $X$ 服从均匀分布时熵最大：&lt;/p&gt;
&lt;p&gt;$$
H(P) = -\sum_x \frac{1}{|X|} \log \frac{1}{|X|} = - \log \frac{1}{|X|} = \log |X|
$$&lt;/p&gt;
&lt;p&gt;直观地说：最大熵原理认为要选择的概率模型首先必须满足已有的事实，即约束条件。在没有更多信息的情况下，那些不确定的部分都是 “等可能的” 。最大熵原理通过 &lt;strong&gt;熵的最大化来表示等可能性&lt;/strong&gt; 。这是因为 “等可能” 不容易操作，而熵则是一个可优化的数值指标。&lt;/p&gt;
&lt;p&gt;将最大熵原理应用到分类即可得到最大熵模型。&lt;/p&gt;
&lt;h3&gt;最大熵模型&lt;/h3&gt;
&lt;p&gt;最大熵模型假设分类模型是一个条件概率分布 $P(Y|X)$，$X$ 为特征，$Y$ 为输出。&lt;/p&gt;
&lt;p&gt;给定一个训练集 $S = { (x^{(1)}, y^{(1)}), (x^{(2)}, y^{(2)}), \ldots, (x^{(m)}, y^{(m)}) }$ 其中 $x$ 为 $n$ 维特征向量， $y$ 为类别输出。我们的目标就是用最大熵模型选择一个最好的分类类型。在给定训练集的情况下，我们可以得到总体联合分布 $P(X,Y)$ 的经验分布 $\bar{P}(X, Y)$ 和边缘分布 $P(X)$ 的经验分布 $\bar{P}(X)$ 。&lt;/p&gt;
&lt;p&gt;其中 $\bar{P}(X, Y)$ 通过训练集中 $X$ 、$Y$ 同时出现的次数除以样本总数 $m$ 来计算，$\bar{P}(X)$ 通过训练集中 $X$ 出现的次数除以样本总数 $m$ 来计算：&lt;/p&gt;
&lt;p&gt;$$
\bar{P}(X = x, Y = y) = \frac{\text{count}(X = x, Y = y)}{N}
$$&lt;/p&gt;
&lt;p&gt;$$
\bar{P}(X = x) = \frac{\text{count}(X = x)}{N}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;特征函数与约束条件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在最大熵模型中，我们通过一组特征函数 $f(x,y)$ 描述输入 $x$ 和输出 $y$ 之间的关系。其形式如下：&lt;/p&gt;
&lt;p&gt;$$
f(x,y) =
\begin{cases}
1 &amp;amp; \text{if certain condition between } x \text{ and } y \text{ holds} \
0 &amp;amp; \text{otherwise}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;每个特征函数对应一个可能的输入输出关系或约束，不同的训练样本可能激活不同的特征函数，并且同一个样本可能激活多个特征函数。&lt;/p&gt;
&lt;p&gt;特征函数 $f(x,y)$ 关于经验分布 $\bar{P}(X, Y)$ 的期望值，用 $\mathbb{E}_{\bar{P}}\big[ f \big]$ 表示:&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}&lt;em&gt;{\bar{P}}\big[ f \big] = \sum&lt;/em&gt;{x, y} \bar{P}(x, y)f(x, y) = \frac{1}{N} \sum_{x, y} f(x, y)
$$&lt;/p&gt;
&lt;p&gt;由于特征函数在构建概率模型时扮演重要角色，我们希望最大熵模型能满足这些约束条件。因此我们要求模型 $P(Y|X)$ 关于函数 $f$ 的期望应该等于经验分布关于 $f$ 的期望。模型 $P(Y|X)$ 关于 $f$ 的期望为：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}&lt;em&gt;{P}\big[ f \big] = \sum&lt;/em&gt;{x, y} P(x, y)f(x, y) ≈ \sum_{x, y} \bar{P}(x)P(y|x)f(x, y)
$$&lt;/p&gt;
&lt;p&gt;因此我们需要使得模型的期望满足以下约束（确保模型的特征函数期望与训练数据一致）：&lt;/p&gt;
&lt;p&gt;$$
\sum_{x, y} \bar{P}(x)P(y|x)f(x, y) = \sum_{x, y} \bar{P}(x, y)f(x, y)
$$&lt;/p&gt;
&lt;p&gt;上述式子便是最大熵模型中所要求满足的约束条件。给定 $n$ 个特征函数 $f_i(x, y)$ ，则有 $n$ 个约束条件。用 $C$ 表示满足约束的模型集合：&lt;/p&gt;
&lt;p&gt;$$
C = { P|\mathbb{E}&lt;em&gt;{P}\big[f_i\big] = \mathbb{E}&lt;/em&gt;{\bar{P}}\big[f_i\big], I = 1, 2, \ldots, n }
$$&lt;/p&gt;
&lt;p&gt;我们通过从满足约束的模型集合 $C$ 中选出熵最大的模型，就可以得到最终的最大熵模型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最大熵模型定义&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于条件分布 $P(Y|X)$ 的熵为：&lt;/p&gt;
&lt;p&gt;$$
H(P) = - \sum_{x, y} P(x, y) \log P(y|x) = - \sum_{x, y} \bar{P}(x)P(y|x) \log P(y|x)
$$&lt;/p&gt;
&lt;p&gt;满足约束条件后使得该熵最大，由此可得 MaxEnt 模型 $P^*$ 为：&lt;/p&gt;
&lt;p&gt;$$
P^* = \arg \max_{P \in C}H(P)
$$&lt;/p&gt;
&lt;p&gt;给定数据集 ${(x_i, y_i)}_{i=1}^N$ ，特征函数 $f_i(x, y) \quad (i = 1, 2 \ldots, n)$ ，根据经验分布得到满足约束集的模型集合：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\min_{P \in C} \quad &amp;amp; \sum_{x,y} \bar{P}(x) P(y|x) \log P(y|x) \
\text{s.t.} \quad &amp;amp; \mathbb{E}&lt;em&gt;p\big[f_i\big] = \mathbb{E}&lt;/em&gt;{\bar{P}}\big[f_i\big] \
&amp;amp; \sum_y P(y|x) = 1
\end{align*}
$$&lt;/p&gt;
&lt;h3&gt;模型求解&lt;/h3&gt;
&lt;p&gt;MaxEnt 模型最后被形式化为带有约束条件的最优化问题，可以通过拉格朗日乘子法将其转为无约束优化的问题，引入拉格朗日乘子 $w_i$ ，定义朗格朗日函数 $L(P, w)$ ：&lt;/p&gt;
&lt;p&gt;$$
L(P, w) = -H(P) + w_0 \left[ 1 - \sum_y P(y|x) \right] + \sum_{i=1}^n w_i \Big(\mathbb{E}_{\bar{P}}\big[f_i\big] - \mathbb{E}_p\big[f_i\big]\Big)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;拉格朗日对偶问题求解最优化问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据拉格朗日对偶问题（如果不知道什么是拉格朗日对偶问题可以看本系列的第五节，只需要看拉格朗日对偶问题相关的介绍即可），我们可以得到下面这个优化问题：&lt;/p&gt;
&lt;p&gt;$$
\max_w \min_{P \in C} L(P, w)
$$&lt;/p&gt;
&lt;p&gt;我们可以先求解内部的极小值问题，记作 $\Psi(w)$ ：&lt;/p&gt;
&lt;p&gt;$$
\Psi(w) = \min_{P \in C} L(P, w) = L(P_w, w)
$$&lt;/p&gt;
&lt;p&gt;上式中得到的 $P_w$ 可以记作：&lt;/p&gt;
&lt;p&gt;$$
P_w = \arg \min_{P \in C} L(P, w) = P_w(y|x)
$$&lt;/p&gt;
&lt;p&gt;由于求解 $P$ 的最小值 $P_w$ ,只需对于 $P(y|x)$ 求导即可,令导数等于 0 即可得到 $P_w(y|x)$ ：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\frac{\partial L(P, w)}{\partial P(y|x)} &amp;amp;= \sum_{x,y} \bar{P}(x) (\log P(y|x) + 1) - \sum_y w_0 - \sum_{x,y} \bar{P}(x) \sum_{i=1}^n w_i f_i(x, y) \
&amp;amp;= \sum_{x,y} \bar{P}(x) \left[ \log P(y|x) + 1 - w_0 - \sum_{i=1}^n w_i f_i(x, y) \right]
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;令导数为 0 可得：&lt;/p&gt;
&lt;p&gt;$$
P(y|x) = \exp\left(\sum_{i=1}^{n} w_i f_i(x, y) + w_0 - 1\right) = \frac{\exp\Big(\sum_{i=1}^{n} w_i f_i(x, y)\Big)}{\exp(1 - w_0)}
$$&lt;/p&gt;
&lt;p&gt;又因为 $\sum_y P(y|x) = 1$ ，可以得到：&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{\exp(1 - w_0)} \sum_y \exp\left(\sum_{i=1}^{n} w_i f_i(x, y)\right) = 1
$$&lt;/p&gt;
&lt;p&gt;进而可以得到：&lt;/p&gt;
&lt;p&gt;$$
\exp(1 - w_0) = \sum_{y} \exp\left(\sum_{i=1}^{n} w_i f_i(x, y)\right)
$$&lt;/p&gt;
&lt;p&gt;这里 $\exp(1 - w_0)$ 起到了归一化的作用。&lt;/p&gt;
&lt;p&gt;令 $Z_w(x)$ 表示 $\exp(1 - w_0)$ ，便得到了 MaxEnt 模型：&lt;/p&gt;
&lt;p&gt;$$
P_w(y|x) = \frac{1}{Z_w(x)} \exp\left(\sum_{i=1}^{n} w_i f_i(x, y)\right)
$$&lt;/p&gt;
&lt;p&gt;$$
Z_w(x) = \sum_{y} \exp\left(\sum_{i=1}^{n} w_i f_i(x, y)\right)
$$&lt;/p&gt;
&lt;p&gt;这里 $f_i(x, y)$ 代表特征函数，$w_i$ 代表特征函数的权值，$P_w(y|x)$ 即为 MaxEnt 模型，现在内部的极小化求解得到关于 $w$ 的函数，现在求其对偶问题的外部极大化即可，将最优解记做 $w^*$ ：&lt;/p&gt;
&lt;p&gt;$$
w^* = \arg\max_{w} \Psi(w)
$$&lt;/p&gt;
&lt;p&gt;所以现在最大上模型转为求解 $\Psi(w)$ 的极大化问题，求解最优的 $w^*$ 后， 便得到了所要求的 MaxEnt 模型，将 $P_w(y|x)$ 带入 $\Psi(w)$ 可得：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\Psi(w) &amp;amp;= \sum_{x,y} \bar{P}(x) P_w(y|x) \log P_w(y|x) + \sum_{i=1}^{n} w_i \left[ \sum_{x,y} \bar{P}(x,y) f(x,y) - \sum_{x,y} \bar{P}(x) P_w(y|x) f(x,y) \right] \
&amp;amp;= \sum_{x,y} \bar{P}(x,y) \sum_{i=1}^{n} w_i f_i(x,y) + \sum_{x,y} \bar{P}(x) P_w(y|x) \left[ \log P_w(y|x) - \sum_{i=1}^{n} w_i f_i(x,y) \right]
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;又因为下述结论：&lt;/p&gt;
&lt;p&gt;$$
P_w(y|x) = \frac{1}{Z_w(x)} \exp\left(\sum_{i=1}^{n} w_i f_i(x, y)\right) \Rightarrow \log P_w(y|x) = \sum_{i=1}^{n} w_i f_i(x, y) - \log Z_w(x)
$$&lt;/p&gt;
&lt;p&gt;因此可以得到：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\Psi(w) &amp;amp;= \sum_{x,y} \bar{P}(x,y) \sum_{i=1}^{n} w_i f_i(x,y) - \sum_{x,y} \bar{P}(x) P_w(y|x) \log Z_w(x) \
&amp;amp;= \sum_{x, y} \bar{P}(x, y) \sum_{i=1}^{n}w_if_i(x, y) - \sum_x \bar{P}(x)\log Z_w(x)
\sum_y P_w(y|x)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;再根据 $\sum_y P_w(y|x) = 1$ 可得最后的优化问题为：&lt;/p&gt;
&lt;p&gt;$$
\max_{w} \Psi(w) = \max_{w} \sum_{x, y} \bar{P}(x, y) \sum_{i=1}^{n}w_if_i(x, y) - \sum_x \bar{P}(x) \log Z_w(x)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;极大似然估计求解最优化问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实我们还可以用极大似然估计的方法求解优化函数，先写出对数似然函数：&lt;/p&gt;
&lt;p&gt;$$
L_{\bar{P}}(P_w) = \sum_{x, y} \bar{P}(x, y) \log P(y|x)
$$&lt;/p&gt;
&lt;p&gt;将上文得到的 $P_w(y|x)$ 的表达式带入对数似然函数可得：&lt;/p&gt;
&lt;p&gt;$$
L_{\bar{P}}(P_w) = \sum_{x, y} \bar{P}(x, y) \sum_{i=1}^{n}w_if_i(x, y) - \sum_x \bar{P}(x) \log Z_w(x)
$$&lt;/p&gt;
&lt;p&gt;显而易见，拉格朗日对偶得到的结果与极大似然得到的结果是等价的。&lt;/p&gt;
&lt;h2&gt;概念介绍&lt;/h2&gt;
&lt;p&gt;设 $V$ 是所有可能的 &lt;strong&gt;观测集合&lt;/strong&gt; ， $Q$ 是所有可能的 &lt;strong&gt;状态集合&lt;/strong&gt; ：&lt;/p&gt;
&lt;p&gt;$$
V = { v_1, v_2, \ldots, v_M }, Q = { q_1, q_2, \ldots, q_N }
$$&lt;/p&gt;
&lt;p&gt;其中 $N$ 式可能的状态数， $M$ 是可能的观测数。&lt;/p&gt;
&lt;p&gt;$O$ 是长度为 $T$ 的 &lt;strong&gt;观测序列&lt;/strong&gt; ， $I$ 是对应的 &lt;strong&gt;状态序列&lt;/strong&gt; ：&lt;/p&gt;
&lt;p&gt;$$
O = { o_1, o_2, \ldots, o_T }, I = { i_1, i_2, \ldots, i_T }
$$&lt;/p&gt;
&lt;p&gt;在已知观测序列 $O$ 的条件下，状态序列为 $I$ 的概率为（下面用到了最大熵模型的结论）：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(I|O) &amp;amp;= P(i_1, i_2, \ldots, i_T | O) = P(i_1 | O) \prod_{t=2}^T P(i_t | i_{t-1}, O) \
&amp;amp;= P(i_1 | O) \prod_{t=2}^T \frac{1}{Z(i_{t-1}, O)} \exp\left(\sum_{k=1}^K w_k f_k(i_t, i_{t-1}, O)\right)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;其中 $Z(i_{t-1}, O) = \sum_{i_t} \exp\left( \sum_{k=1}^{K} w_kf_k(i_t, i_{t-1}, O) \right)$ 、$f_k(i_t, i_{t-1}, O)$ 和 $w_k$ 分别对应于最大熵模型中的归一化因子、特征函数和特征函数的权重。&lt;/p&gt;
&lt;p&gt;在 $i_1$ 前添加一个恒为常量 0 的状态 $i_0$ ，则上式可化简为：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
P(I|O) &amp;amp;= P(i_1, i_2, \ldots, i_T | O) = \prod_{t=2}^T P(i_t | i_{t-1}, O) \
&amp;amp;= \prod_{t=2}^T \frac{1}{Z(i_{t-1}, O)} \exp\left(\sum_{k=1}^K w_k f_k(i_t, i_{t-1}, O)\right)
\end{align*}
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;最大熵马尔可夫模型实现难点&lt;/h1&gt;
&lt;p&gt;在最大熵马尔可夫模型（MEMM）中，我们同样需要围绕三类基本问题展开讨论：如何计算条件概率、如何从标注数据中学习参数以及如何根据观测推断最可能的状态序列。整体结构与 HMM 非常相似，只不过 MEMM 直接建模条件概率，因此在处理方式上具有明显的判别式特点。&lt;/p&gt;
&lt;p&gt;首先是 &lt;strong&gt;计算问题&lt;/strong&gt; 。当模型参数 $w_k(k=1,2,\ldots,K)$ 、观测序列 $O=(o_1,o_2,\ldots,o_T)$ 以及某条给定的状态序列 $I=(i_1,i_2,\ldots,i_T)$ 已知时，我们希望求得该序列在给定观测条件下的条件概率 $P(I | O)$ 。在 MEMM 中，这个概率是由一系列局部的条件分布组合而成，每一步的转移概率通常写成最大熵形式，即通过一组特征函数和对应的权重参数构造出的指数族模型，并在每一时刻进行归一化。整个序列的条件概率就是这些局部概率的连乘结果。&lt;/p&gt;
&lt;p&gt;其次是 &lt;strong&gt;学习问题&lt;/strong&gt; 。与 HMM 的无监督学习不同，MEMM 的学习是基于观察序列与状态序列的成对标注数据，目标是最大化条件概率 $P(I | O)$ ，即最大化条件对数似然。在这一过程中，我们往往需要加入正则化来避免过拟合，并使用数值优化方法（如梯度下降、L-BFGS 等）直接求解参数，使模型在给定上下文与特征的条件下，尽可能提高对真实标签的条件概率。可以将其理解为在序列结构上应用逻辑回归或最大熵分类器。&lt;/p&gt;
&lt;p&gt;最后是 &lt;strong&gt;预测问题&lt;/strong&gt; 。当模型参数已经确定且观测序列已知时，我们希望找到使条件概率 $P(I | O)$ 最大的状态路径。由于 MEMM 的结构与 HMM 的动态规划框架兼容，我们依然可以使用类似 Viterbi 的递推方法，在每个时刻根据前一状态与当前观测计算局部条件概率，并利用动态规划找到一条整体概率最高的路径。在一些任务中，人们也会采用束搜索等近似方法，以在计算效率与预测效果之间取得平衡。&lt;/p&gt;
&lt;p&gt;综上所述，这三个问题分别对应 MEMM 的三类核心任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;条件概率计算&lt;/strong&gt;：利用局部最大熵模型计算 $P(I | O)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数学习&lt;/strong&gt;：最大化条件对数似然以求得最优 $w_k$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列解码&lt;/strong&gt;：通过动态规划或搜索找到最可能的状态序列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在理解了这些问题及其之间的对应关系后，我们就可以进一步展开它们各自的求解策略与算法细节。&lt;/p&gt;
&lt;h2&gt;计算问题&lt;/h2&gt;
&lt;p&gt;由于 MEMM 属于判别式模型，对于判别式模型来说，给定了模型参数 $w_k(k = 1, 2, \ldots, K)$ 和观测序列 $O = (o_1, o_2, \ldots, o_T)$ ，直接套用模型的定义就可以计算出条件概率 $P(I|O)$ 。&lt;/p&gt;
&lt;h2&gt;学习问题&lt;/h2&gt;
&lt;p&gt;既有观测序列 $O = (o_1, o_2, \ldots, o_T)$ 也有状态序列 $I = (i_1, i_2, \ldots, i_T)$ 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 MEMM 类似于最大熵模型，所以能用于估计最大熵模型参数的策略和算法均可用于 MEMM。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有观测序列 $O = (o_1, o_2, \ldots, o_T)$ 而没有状态序列 $I = (i_1, i_2, \ldots, i_T)$ 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时 MEMM 是一个含有隐变量的模型，对于含有隐变量的模型，则可以使用 EM 算法对其进行参数估计。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;预测问题&lt;/h2&gt;
&lt;p&gt;我们在上一篇文章中说到的 Viterbi 算法一样可以用在 MEMM 的预测问题中，具体算法如下：&lt;/p&gt;
&lt;p&gt;定义在时刻 $t$ 状态为 $q_i$ 的所有单个路径 $(i_1, i_2, \ldots, i_t)$ 中概率最大值为：&lt;/p&gt;
&lt;p&gt;$$
\delta_t(i) = \max_{i_1, i_2, \ldots, i_{t-1}} P(i_1, \ldots, i_{t-1}, i_t = q_i | O) \quad i = 1, 2, \ldots, N
$$&lt;/p&gt;
&lt;p&gt;由此定义可推得：&lt;/p&gt;
&lt;p&gt;$$
\delta_1(i) = P(i_1 = q_i|i_0 = 0, O)
$$&lt;/p&gt;
&lt;p&gt;$$
\delta_2(i) = \max_{1 \leq j \leq N} \big[\delta_1(j) \cdot P(i_2 = q_i|i_1 = q_j, O)\big]
$$&lt;/p&gt;
&lt;p&gt;$$
\delta_3(i) = \max_{1 \leq j \leq N} \big[\delta_2(j) \cdot P(i_3 = q_i|i_2 = q_j, O)\big]
$$&lt;/p&gt;
&lt;p&gt;依次此类推可得如下递推公式：&lt;/p&gt;
&lt;p&gt;$$
\delta_t(i) = \max_{1 \leq j \leq N} \big[\delta_{t-1}(j) \cdot P(i_t = q_i|i_{t-1} = q_j, O)\big]
$$&lt;/p&gt;
&lt;p&gt;同样再定义在时刻 $t$ 状态为 $q_i$ 的所有单个路径 $(i_1, i_2, \ldots, i_t)$ 中概率最大的路径的第 $t-1$ 个节点为：&lt;/p&gt;
&lt;p&gt;$$
\Psi_t(i) = \arg\max_{1 \leq j \leq N} \big[\delta_{t-1}(j) \cdot P(i_t = q_i | i_{t-1} = q_j, O)\big]
$$&lt;/p&gt;
&lt;p&gt;令 $i_T^* = \arg \max_{1 \leq i \leq N} \delta_T(i)$ 可得：&lt;/p&gt;
&lt;p&gt;$$
i_{T-1}^* = \Psi_T(i_T^&lt;em&gt;), i_{T-2}^&lt;/em&gt; = \Psi_{T-1}(i_{T-1}^&lt;em&gt;), \ldots, i_1^&lt;/em&gt; = \Psi_2(i_2^*)
$$&lt;/p&gt;
&lt;h3&gt;标注偏置问题&lt;/h3&gt;
&lt;p&gt;由于 MEMM 模型本身的问题，用维特比算法求出来的最优序列 $I^* = (i_1^&lt;em&gt;, i_2^&lt;/em&gt;, \ldots, i_T^*)$ &lt;strong&gt;并不是真正意义上的最优状态序列&lt;/strong&gt; ，下面举例说明。&lt;/p&gt;
&lt;p&gt;假设已知的观测序列为 $O = (o_1, o_2, o_3, o_4)$ ，所有可能的状态的集合为 $O = (1, 2, 3, 4, 5)$ ，各个时刻之间的状态转移概率如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cmaximum-entropy-markov%5C%E6%9C%80%E5%A4%A7%E7%86%B5%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B2.png&quot; alt=&quot;最大熵马尔可夫模型图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由维特比算法易算得最优状态序列 $I^* = (1, 1, 1, 1)$ ，但是结合的实际情形可知，状态序列 $\bar{I} = (1, 2, 2, 2)$ 显然比 $I^&lt;em&gt;$ 更加合理，这是因为 $\bar{I}$ 每个时刻之间的状态转移都比 $I^&lt;/em&gt;$ 更加 &lt;strong&gt;自信&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;因此 $I^*$ 并不一定是真正意义上的最优状态序列，而这就是 MEMM 的 &lt;strong&gt;标注偏置问题&lt;/strong&gt; （The Label Bias Problem）。&lt;/p&gt;
&lt;p&gt;导致标注偏置问题的主要原因是 MEMM 对各个时刻的状态取值的概率 $P(i_t|i_{t-1}, o_t)$ 都进行了局部归一化，也就是：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i_t} P(i_t | i_{t-1}, O) = \sum_{i_t} \frac{1}{Z(i_{t-1}, O)} \exp\left(\sum_{k=1}^K w_k f_k(i_t, i_{t-1}, O)\right) = 1
$$&lt;/p&gt;
&lt;p&gt;显然进行局部归一化后，对于那些可转移状态较少的状态来说，它们转移到下一个状态的概率通常都会比那些可转移状态多的状态转到下一个状态的概率要高，因此可转移状态较少的状态更可能被算法选中。如果要解决标注偏置问题，只需取消局部归一化或者换成全局归一化即可解决。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;最大熵马尔可夫模型代码讲解&lt;/h1&gt;
&lt;p&gt;下面给出 MEMM 的代码，具体原理自行观看上述证明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import List
import numpy as np
from sklearn.feature_extraction import DictVectorizer
from sklearn.linear_model import LogisticRegression
train_sents = [
    [&apos;John&apos;, &apos;loves&apos;, &apos;Mary&apos;],
    [&apos;Mary&apos;, &apos;hates&apos;, &apos;Bob&apos;],
    [&apos;Bob&apos;, &apos;likes&apos;, &apos;Alice&apos;],
]
train_tags = [
    [&apos;NNP&apos;, &apos;VBZ&apos;, &apos;NNP&apos;],
    [&apos;NNP&apos;, &apos;VBZ&apos;, &apos;NNP&apos;],
    [&apos;NNP&apos;, &apos;VBZ&apos;, &apos;NNP&apos;],
]


def default_feature_extractor(sentence: List[str], i: int, prev_tag: str) -&amp;gt; dict:
    token = sentence[i]
    features = {}
    features[f&quot;word={token}&quot;] = 1
    features[f&quot;word_lower={token.lower()}&quot;] = 1
    if len(token) &amp;gt;= 3:
        features[f&quot;suffix3={token[-3:]}&quot;] = 1
    features[f&quot;is_title={token[0].isupper()}&quot;] = 1
    features[f&quot;is_digit={token.isdigit()}&quot;] = 1
    # 前后词
    if i &amp;gt; 0:
        features[f&quot;prev_word={sentence[i-1]}&quot;] = 1
    else:
        features[&quot;BOS&quot;] = 1
    if i &amp;lt; len(sentence)-1:
        features[f&quot;next_word={sentence[i+1]}&quot;] = 1
    else:
        features[&quot;EOS&quot;] = 1
    # 把前一标签也作为一个特征（MEMM 的关键点）
    features[f&quot;prev_tag={prev_tag}&quot;] = 1
    return features

class MEMM:
    def __init__(self, feature_extractor=default_feature_extractor, solver=&apos;lbfgs&apos;, max_iter=200):
        self.feature_extractor = feature_extractor
        self.vec = DictVectorizer(sparse=True)
        self.clf = LogisticRegression(multi_class=&apos;multinomial&apos;, solver=solver, max_iter=max_iter)
        self.label_to_index = {}
        self.index_to_label = []
        self.fitted = False

    def _gather_training_instances(self, sents: List[List[str]], tags: List[List[str]]):
        X_dicts = []; y = []
        for sent, tag_seq in zip(sents, tags):
            for i in range(len(sent)):
                prev_tag = tag_seq[i-1] if i &amp;gt; 0 else &apos;&amp;lt;START&amp;gt;&apos;
                feats = self.feature_extractor(sent, i, prev_tag)
                X_dicts.append(feats)
                y.append(tag_seq[i])
        return X_dicts, y

    # 既有观测序列也有状态序列
    def fit(self, sents: List[List[str]], tags: List[List[str]]):
        X_dicts, y = self._gather_training_instances(sents, tags)
        # 记录标签映射
        labels = sorted(set(y))
        self.index_to_label = labels
        self.label_to_index = {lab: i for i, lab in enumerate(labels)}
        # vectorize
        X = self.vec.fit_transform(X_dicts)
        y_idx = np.array([self.label_to_index[lab] for lab in y])
        # 训练分类器
        self.clf.fit(X, y_idx)
        self.fitted = True
        return self

    def _local_log_probs(self, sentence: List[str], position: int, prev_tag: str) -&amp;gt; np.ndarray:
        feats = self.feature_extractor(sentence, position, prev_tag)
        X = self.vec.transform([feats])
        logp = self.clf.predict_log_proba(X)[0]
        return logp

    def viterbi(self, sentence: List[str]) -&amp;gt; List[str]:
        assert self.fitted, &quot;模型尚未训练，请先调用 fit()&quot;
        n_tags = len(self.index_to_label)
        T = len(sentence)
        # dp[t, j] = 最佳路径到位置 t 且标签为 j 的对数概率
        dp = np.full((T, n_tags), -np.inf)
        backptr = np.zeros((T, n_tags), dtype=int)
        # 初始步 t=0，prev_tag = &apos;&amp;lt;START&amp;gt;&apos;
        for j in range(n_tags):
            cur_tag = self.index_to_label[j]
            logp = self._local_log_probs(sentence, 0, &apos;&amp;lt;START&amp;gt;&apos;)
            dp[0, j] = logp[j]
            backptr[0, j] = -1

        # 递推
        for t in range(1, T):
            for j in range(n_tags):
                cur_tag = self.index_to_label[j]
                best_score = -np.inf
                best_prev = 0
                # 对每个可能的前一标签 i
                for i in range(n_tags):
                    prev_tag = self.index_to_label[i]
                    # 计算在 prev_tag 下转移到 cur_tag 的 log 概率
                    logp = self._local_log_probs(sentence, t, prev_tag)
                    score = dp[t-1, i] + logp[j]
                    if score &amp;gt; best_score:
                        best_score = score
                        best_prev = i
                dp[t, j] = best_score
                backptr[t, j] = best_prev

        # 回溯
        best_last = int(np.argmax(dp[T-1]))
        tags_idx = [best_last]
        for t in range(T-1, 0, -1):
            best_prev = backptr[t, tags_idx[-1]]
            tags_idx.append(int(best_prev))
        tags_idx.reverse()
        return [self.index_to_label[i] for i in tags_idx]

    def predict(self, sentence: List[str]) -&amp;gt; List[str]:
        return self.viterbi(sentence)


# 执行代码
if __name__ == &apos;__main__&apos;:
    memm = MEMM()
    memm.fit(train_sents, train_tags)

    test = [&apos;Alice&apos;, &apos;loves&apos;, &apos;Bob&apos;]
    print(&apos;Test sentence:&apos;, test)
    pred = memm.predict(test)
    print(&apos;Predicted tags:&apos;, pred)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 学习问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#%E5%AD%A6%E4%B9%A0%E9%97%AE%E9%A2%98&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def default_feature_extractor(sentence: List[str], i: int, prev_tag: str) -&amp;gt; dict:
    token = sentence[i]
    features = {}
    features[f&quot;word={token}&quot;] = 1
    features[f&quot;word_lower={token.lower()}&quot;] = 1
    if len(token) &amp;gt;= 3:
        features[f&quot;suffix3={token[-3:]}&quot;] = 1
    features[f&quot;is_title={token[0].isupper()}&quot;] = 1
    features[f&quot;is_digit={token.isdigit()}&quot;] = 1
    # 前后词
    if i &amp;gt; 0:
        features[f&quot;prev_word={sentence[i-1]}&quot;] = 1
    else:
        features[&quot;BOS&quot;] = 1
    if i &amp;lt; len(sentence)-1:
        features[f&quot;next_word={sentence[i+1]}&quot;] = 1
    else:
        features[&quot;EOS&quot;] = 1
    # 把前一标签也作为一个特征（MEMM 的关键点）
    features[f&quot;prev_tag={prev_tag}&quot;] = 1
    return features

def _gather_training_instances(self, sents: List[List[str]], tags: List[List[str]]):
    X_dicts = []; y = []
    for sent, tag_seq in zip(sents, tags):
        for i in range(len(sent)):
            prev_tag = tag_seq[i-1] if i &amp;gt; 0 else &apos;&amp;lt;START&amp;gt;&apos;
            feats = self.feature_extractor(sent, i, prev_tag)
            X_dicts.append(feats)
            y.append(tag_seq[i])
    return X_dicts, y

# 既有观测序列也有状态序列
def fit(self, sents: List[List[str]], tags: List[List[str]]):
    X_dicts, y = self._gather_training_instances(sents, tags)
    # 记录标签映射
    labels = sorted(set(y))
    self.index_to_label = labels
    self.label_to_index = {lab: i for i, lab in enumerate(labels)}
    # vectorize
    X = self.vec.fit_transform(X_dicts)
    y_idx = np.array([self.label_to_index[lab] for lab in y])
    # 训练分类器
    self.clf.fit(X, y_idx)
    self.fitted = True
    return self
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 预测问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#%E9%A2%84%E6%B5%8B%E9%97%AE%E9%A2%98&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def _local_log_probs(self, sentence: List[str], position: int, prev_tag: str) -&amp;gt; np.ndarray:
    feats = self.feature_extractor(sentence, position, prev_tag)
    X = self.vec.transform([feats])
    logp = self.clf.predict_log_proba(X)[0]
    return logp

def viterbi(self, sentence: List[str]) -&amp;gt; List[str]:
    assert self.fitted, &quot;模型尚未训练，请先调用 fit()&quot;
    n_tags = len(self.index_to_label)
    T = len(sentence)
    # dp[t, j] = 最佳路径到位置 t 且标签为 j 的对数概率
    dp = np.full((T, n_tags), -np.inf)
    backptr = np.zeros((T, n_tags), dtype=int)
    # 初始步 t=0，prev_tag = &apos;&amp;lt;START&amp;gt;&apos;
    for j in range(n_tags):
        cur_tag = self.index_to_label[j]
        logp = self._local_log_probs(sentence, 0, &apos;&amp;lt;START&amp;gt;&apos;)
        dp[0, j] = logp[j]
        backptr[0, j] = -1

    # 递推
    for t in range(1, T):
        for j in range(n_tags):
            cur_tag = self.index_to_label[j]
            best_score = -np.inf
            best_prev = 0
            # 对每个可能的前一标签 i
            for i in range(n_tags):
                prev_tag = self.index_to_label[i]
                # 计算在 prev_tag 下转移到 cur_tag 的 log 概率
                logp = self._local_log_probs(sentence, t, prev_tag)
                score = dp[t-1, i] + logp[j]
                if score &amp;gt; best_score:
                    best_score = score
                    best_prev = i
            dp[t, j] = best_score
            backptr[t, j] = best_prev

    # 回溯
    best_last = int(np.argmax(dp[T-1]))
    tags_idx = [best_last]
    for t in range(T-1, 0, -1):
        best_prev = backptr[t, tags_idx[-1]]
        tags_idx.append(int(best_prev))
    tags_idx.reverse()
    return [self.index_to_label[i] for i in tags_idx]

def predict(self, sentence: List[str]) -&amp;gt; List[str]:
    return self.viterbi(sentence)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么最大熵模型最大化的是条件熵？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;问题引用自该话题：&lt;a href=&quot;https://www.zhihu.com/question/35295907&quot;&gt;最大熵模型，为什么最大的是条件熵？&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最大熵原理的目标是选择一个 &lt;strong&gt;不做额外假设、不偏不倚&lt;/strong&gt; 的模型，在给定已知条件下，保留最大的不确定性。&lt;/p&gt;
&lt;p&gt;最大熵模型的核心是让模型在已知输入 $X$ 的条件下，尽可能保持 &lt;strong&gt;最大的不确定性&lt;/strong&gt; 。这就意味着，给定输入 $X$ ，我们不希望模型对输出 $Y$ 做出过于确定的猜测，除非有足够的证据支持某些标签。&lt;/p&gt;
&lt;p&gt;条件熵 $H(Y|X)$ 衡量的是：在给定 $X$ 的条件下， $Y$ 的不确定性。通过最大化条件熵，我们实际上是在 &lt;strong&gt;最大化所有可能输出 $Y$ 的不确定性&lt;/strong&gt; ，并且通过约束条件来确保它符合训练数据中的真实模式。&lt;/p&gt;
&lt;p&gt;最大化条件熵能保证我们选择的模型在已知信息的条件下保持最小的偏向性，从而避免引入不必要的假设。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;最大熵模型&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/pinard/p/6093948.html&quot;&gt;最大熵模型原理小结&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_43764974/article/details/147196896&quot;&gt;【maxENT】最大熵模型（Maximum Entropy Model）介绍与使用&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/ooon/p/5677098.html&quot;&gt;最大熵模型 Maximum Entropy Model&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/136710858&quot;&gt;最大熵模型-Max Entropy Model&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/717650932&quot;&gt;最大熵模型（Maximum Entropy Model, MaxEnt）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/BlairGrowing/p/14906291.html&quot;&gt;机器学习——最大熵模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_47190374/article/details/136971135&quot;&gt;最大熵模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最大熵马尔可夫&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/sinat_34072381/article/details/107279644&quot;&gt;最大熵模型（ME）和最大熵马尔可夫模型（MEMM）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sm1les.com/2019/07/26/maximum-entropy-markov-model/&quot;&gt;最大熵马尔可夫模型（MEMM）及其三个基本问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/sinat_34072381/article/details/107279644&quot;&gt;最大熵模型（ME）和最大熵马尔可夫模型（MEMM）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/717650932&quot;&gt;最大熵模型（Maximum Entropy Model, MaxEnt）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/113187662&quot;&gt;最大熵马尔可夫模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://allenwind.github.io/blog/13551&quot;&gt;序列标注：从HMM、MEMM到CRF&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://allenwind.github.io/blog/7694&quot;&gt;概率图模型系列（4）：MEMM&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/259660645&quot;&gt;【归纳综述】马尔可夫、隐马尔可夫 HMM 、条件随机场 CRF&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第七节：隐马尔可夫模型</title><link>https://xingguang641.com/posts/hidden-markov-model/hidden-markov-model/</link><guid isPermaLink="true">https://xingguang641.com/posts/hidden-markov-model/hidden-markov-model/</guid><description>介绍机器学习常见的模型</description><pubDate>Thu, 30 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：隐马尔可夫模型是机器学习基本模型中的第二个大难点，也是我们讲到的第一个概率图模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;隐马尔可夫模型基本原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;隐马尔可夫模型&lt;/strong&gt;（Hidden Markov Model，简称 HMM）是一种用于时序数据分析的 &lt;strong&gt;概率图模型&lt;/strong&gt; 。它刻画了一个由隐藏状态组成的马尔可夫链，这个链在时间上生成一个不可直接观测的 &lt;strong&gt;状态序列&lt;/strong&gt; ，并且每个状态都会根据一定的概率分布产生一个可观测的输出，从而形成 &lt;strong&gt;观测序列&lt;/strong&gt; 。换句话说：HMM 描述了隐藏的状态在时间上按马尔可夫过程演化，而每个时刻的观测值则由对应的隐藏状态随机生成。其形式定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Chidden-markov-model%5C%E9%9A%90%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B1.png&quot; alt=&quot;隐马尔可夫模型1&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;概念介绍&lt;/h2&gt;
&lt;p&gt;$Q$ 是所有可能的 &lt;strong&gt;状态集合&lt;/strong&gt; ， $V$ 是所有可能的 &lt;strong&gt;观测集合&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
Q = { q_1, q_2, \ldots, q_N } \quad V = { v_1, v_2, \ldots, v_M }
$$&lt;/p&gt;
&lt;p&gt;其中 $N$ 是可能的状态数， $M$ 是可能的观测数。&lt;/p&gt;
&lt;p&gt;$I$ 是长度为 $T$ 的 &lt;strong&gt;状态序列&lt;/strong&gt; ， $O$ 是对应的 &lt;strong&gt;观测序列&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
I = { i_1, i_2, \ldots, i_T } \quad O = { o_1, o_2, \ldots, o_T }
$$&lt;/p&gt;
&lt;p&gt;$A$ 是 &lt;strong&gt;状态转移概率矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
A = [a_{ij}]_{N \times N}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } a_{ij} = P(i_{t+1} = q_j|i_t = q_i) \quad i = 1, 2, \ldots, N \quad j = 1, 2,  \ldots, N
$$&lt;/p&gt;
&lt;p&gt;$a_{ij}$ 表示在时刻 $t$ 处于状态 $q_i$ 的条件下在时刻 $t+1$ 转移到状态 $q_j$ 的概率。&lt;/p&gt;
&lt;p&gt;$B$ 是 &lt;strong&gt;观测概率矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
B = [a_{jk}]_{N \times M}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } b_{jk} = P(o_t = v_k|i_t = q_j) \quad j = 1, 2, \ldots, N \quad k = 1, 2,  \ldots, M
$$&lt;/p&gt;
&lt;p&gt;$b_{jk}$ 表示在时刻 $t$ 处于状态 $q_j$ 的条件下生成观测 $v_k$ 的概率。&lt;/p&gt;
&lt;p&gt;$\pi$ 是 &lt;strong&gt;初始状态概率向量&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\pi = (\pi_1, \pi_2, \ldots, \pi_N)
$$&lt;/p&gt;
&lt;p&gt;$$
\text{where } \pi_i = P(i_1 = q_i) \quad i = 1, 2, \ldots, N
$$&lt;/p&gt;
&lt;p&gt;$\pi_i$ 表示时刻 $t = 1$ 时处于状态 $q_i$ 的概率。&lt;/p&gt;
&lt;p&gt;隐马尔可夫模型由初始状态概率向量 $\pi$ 、状态转移概率矩阵 $A$ 和观测概率矩阵 $B$ 决定。 $\pi$ 和 $A$ 决定状态序列， $B$ 决定观测序列。因此，隐马尔可夫模型 $\lambda$ 可以用三元符号表示：&lt;/p&gt;
&lt;p&gt;$$
\lambda = (A, B, \pi)
$$&lt;/p&gt;
&lt;p&gt;从定义可知，隐马尔可夫模型作了两个 &lt;strong&gt;基本假设&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;齐次马尔可夫性假设，即假设隐藏的马尔可夫链在任意时刻 $t$ 的状态只依赖于其前一时刻的状态，与其他时刻的状态及观测无关，也与时刻 $t$ 无关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(i_t|i_{t-1}, o_{t-1}, \ldots, i_1, o_1) = P(i_t|i_{t-1})
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;观测独立性假设，即假设任意时刻的观测只依赖于该时刻的马尔可夫链的状态，与其他观测及状态无关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(o_t|i_T, o_T, \ldots, i_t, i_{t-1}, o_{t-1} \ldots, i_1, o_1) = P(o_t|i_t)
$$&lt;/p&gt;
&lt;p&gt;这两个假设都可以通过&lt;a href=&quot;#%E9%9A%90%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86&quot;&gt;上面&lt;/a&gt;的概率图来理解，每一个条 &lt;strong&gt;有向边&lt;/strong&gt; 表示一个依赖关系（条件概率），只有有向边尾部的状态会影响有向边头部的状态/观测。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;隐马尔可夫模型实现难点&lt;/h1&gt;
&lt;p&gt;在构建隐马尔可夫模型（HMM）时，通常需要围绕三个基本问题展开：如何计算某个观测序列在模型下出现的概率、如何根据观测数据来学习模型参数，以及如何根据观测推断出最可能的隐藏状态序列。这三个问题共同构成了 HMM 的理论基础，也是实际应用中最常遇到的任务。&lt;/p&gt;
&lt;p&gt;首先是 &lt;strong&gt;计算问题&lt;/strong&gt; 。当模型参数 $\lambda = (A, B, \pi)$ 给定时，我们希望知道某个观测序列 $O = (o_1, o_2, \ldots, o_T)$ 在该模型下出现的概率 $P(O|\lambda)$ 。这一步的意义在于衡量模型对数据的解释能力，是后续参数估计与推断的重要前置工作。通常，这个问题会通过前向算法或后向算法来有效求解。&lt;/p&gt;
&lt;p&gt;其次是 &lt;strong&gt;学习问题&lt;/strong&gt; 。在很多实际场景中，我们只观测到了序列本身，而不了解模型参数，因此需要根据观察到的 $O$ 来反推出一组最合适的参数 $\lambda = (A, B, \pi)$ 。目标是让模型最大程度地 “解释” 我们看到的数据，即使得 $P(O|\lambda)$ 最大。这实际上就是一个极大似然估计问题，而经典的解决方法则是 Baum–Welch 算法，也就是 EM 算法在 HMM 上的具体应用形式。&lt;/p&gt;
&lt;p&gt;最后是 &lt;strong&gt;预测问题&lt;/strong&gt; 。即使我们掌握了模型和观测序列，也仍然不知道隐藏状态序列 $I = (i_1, i_2, \ldots, i_T)$ 是如何演化的。解码问题要做的，就是在给定 $O$ 和 $\lambda$ 的条件下，找到能最大化条件概率 $P(I|O)$ 的那一条状态路径。常用的方法是 Viterbi 算法，它能高效地求出一条最可能的状态序列。&lt;/p&gt;
&lt;p&gt;综上所述，这三个问题分别对应 HMM 的三类核心任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算概率&lt;/strong&gt;：求 $P(O|\lambda)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;估计参数&lt;/strong&gt;：最大化 $P(O|\lambda)$ 所需的 $A, B, \pi$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解码序列&lt;/strong&gt;：找到最可能的隐藏状态路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在明确了它们的意义和关系后，我们就可以更系统地介绍对应的算法与求解流程。这样不仅让 HMM 的结构更加清晰，也为后续的应用奠定了扎实的理解基础。&lt;/p&gt;
&lt;h2&gt;计算问题&lt;/h2&gt;
&lt;p&gt;对于前/后向算法来说，只看下面的讲解理解起来可能会较为困难，请自行结合概率图状态转移的思路进行理解。值得注意的是，前/后向算法本质是一个动态规划算法，因此了解动态规划对理解前/后向算法有帮助。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面的博客也有前/后向算法的详细介绍&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_44648285/article/details/146015265&quot;&gt;隐马尔可夫模型（HMM）三大基础问题之——评估问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/144448849&quot;&gt;HMM 隐马尔可夫模型（概率计算算法）&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;直接计算&lt;/h3&gt;
&lt;p&gt;对于求 $P(O|\lambda)$ 最直接的方法就是按照概率公式直接计算：&lt;/p&gt;
&lt;p&gt;$$
P(O|\lambda) = \sum_{I} P(O, I|\lambda) = \sum_{I} P(O|I, \lambda)P(I|\lambda)
$$&lt;/p&gt;
&lt;p&gt;$P(I|\lambda)$ 表示给定模型参数时，产生状态序列 $I = (i_1, i_2, \ldots, i_T)$ 的概率：&lt;/p&gt;
&lt;p&gt;$$
P(I|\lambda) = \pi_{i_1} \prod_{t=1}^{T-1} a_{i_t i_{t+1}}
$$&lt;/p&gt;
&lt;p&gt;$P(O|I, \lambda)$ 表示给定模型参数且产生状态序列 $I = (i_1, i_2, \ldots, i_T)$ 时，产生观测序列 $O = (o_1, o_2, \ldots, o_T)$ 的概率：&lt;/p&gt;
&lt;p&gt;$$
P(O|I, \lambda) = \prod_{t=1}^{T} b_{i_to_t}
$$&lt;/p&gt;
&lt;p&gt;综上可得：&lt;/p&gt;
&lt;p&gt;$$
P(O|\lambda) = \sum_{I} P(O|I, \lambda)P(I|\lambda) = \sum_{i_1, i_2, \ldots, i_T} \pi_{i_1} \left( \prod_{t=1}^{T-1} b_{i_to_t}a_{i_ti_{t+1}} \right) b_{i_To_T}
$$&lt;/p&gt;
&lt;p&gt;其中 $T$ 重循环的时间复杂度为 $O(N^T)$ ，每次循环又要花费 $O(T)$ 的时间计算 $T$ 重循环中的内容，因此总复杂度为 $O(TN^T)$ ，显然这在实际运用中是无法接受的。&lt;/p&gt;
&lt;h3&gt;前向算法&lt;/h3&gt;
&lt;p&gt;首先定义 &lt;strong&gt;前向概率&lt;/strong&gt; ：给定隐马尔可夫模型 $\lambda$ ，定义到时刻 $t$ 部分观测序列 $O = (o_1, o_2, \ldots, o_t)$ 且状态为 $q_i$ 的概率为前向概率，记作：&lt;/p&gt;
&lt;p&gt;$$
\alpha_t(i) = P(o_1, o_2, \ldots, o_t, i_t = q_i|\lambda)
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Chidden-markov-model%5C%E5%89%8D%E5%90%91%E7%AE%97%E6%B3%951.png&quot; alt=&quot;前向算法图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据前向概率的定义可推得：&lt;/p&gt;
&lt;p&gt;$$
P(O|\lambda) = \sum_{i=1}^{N} P(o_1, o_2, \ldots, o_T, i_T = q_i | \lambda) = \sum_{i=1}^{N} \alpha_T(i)
$$&lt;/p&gt;
&lt;p&gt;于是求解 $P(O|\lambda)$ 的问题被转化为了求解前向概率 $\alpha_T(i)$ 的问题。&lt;/p&gt;
&lt;p&gt;由前向概率的定义可知：&lt;/p&gt;
&lt;p&gt;$$
\alpha_1(i) = \pi_1b_{io_1}
$$&lt;/p&gt;
&lt;p&gt;$$
\alpha_2(i) = \left[ \sum_{j=1}^{N} \alpha_1(j)a_{ji} \right] b_{io_2}
$$&lt;/p&gt;
&lt;p&gt;$$
\alpha_3(i) = \left[ \sum_{j=1}^{N} \alpha_2(j)a_{ji} \right] b_{io_3}
$$&lt;/p&gt;
&lt;p&gt;依次此类推可得如下递推公式：&lt;/p&gt;
&lt;p&gt;$$
\alpha_{t+1}(i) = \left[ \sum_{j=1}^{N} \alpha_t(j)a_{ji} \right]j b_{io_{t+1}}
$$&lt;/p&gt;
&lt;h3&gt;后向算法&lt;/h3&gt;
&lt;p&gt;同前向算法一样，首先定义 &lt;strong&gt;后向概率&lt;/strong&gt; ：给定隐马尔可夫模型 $\lambda$ ，定义在时刻 $t$ 状态为 $q_i$ 的条件下，从 $t+1$ 到 $T$ 的部分观测序列为 $O = (o_{t+1}, o_{t+2}, \ldots, o_T)$ 的概率为后向概率，记作：&lt;/p&gt;
&lt;p&gt;$$
\beta_t(i) = P(o_{t+1}, o_{t+2}, \ldots, o_T|i_t = q_i, \lambda)
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Chidden-markov-model%5C%E5%90%8E%E5%90%91%E7%AE%97%E6%B3%951.png&quot; alt=&quot;后向算法图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据后向概率的定义可推得（将后向概率转为前向概率后带入前向概率的公式）：&lt;/p&gt;
&lt;p&gt;$$
P(O|\lambda) = \sum_{i=1}^{N} \pi_i b_{io_1} \beta_1(i)
$$&lt;/p&gt;
&lt;p&gt;由后向概率的定义可知：&lt;/p&gt;
&lt;p&gt;$$
\beta_T(i) = 1
$$&lt;/p&gt;
&lt;p&gt;$$
\beta_{T-1}(i) = \sum_{j=1}^{N} a_{ij}b_{jo_{T}} \beta_T(j)
$$&lt;/p&gt;
&lt;p&gt;$$
\beta_{T-2}(i) = \sum_{j=1}^{N} a_{ij}b_{jo_{T-1}} \beta_{T-1}(j)
$$&lt;/p&gt;
&lt;p&gt;依次此类推可得如下递推公式：&lt;/p&gt;
&lt;p&gt;$$
\beta_t(i) = \sum_{j=1}^{N} a_{ij}b_{jo_{t+1}} \beta_{t+1}(j)
$$&lt;/p&gt;
&lt;p&gt;综上可知前向算法和后向算法都是先计算局部概率，然后递推到全局，每一时刻的概率计算都会用上前一时刻计算出的结果，整体的时间复杂度大约为 O(TN^2) ，明显优于暴力计算的时间复杂度。&lt;/p&gt;
&lt;h3&gt;算法推广&lt;/h3&gt;
&lt;p&gt;利用前向概率和后向概率，可以得到关于单个状态和两个状态概率的计算公式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;给定模型参数 $\lambda$ 和观测 $O$ ，在时刻 $t$ 处于状态 $q_i$ 的概率，记：&lt;/p&gt;
&lt;p&gt;$$
\gamma_t(i) = P(i_t = q_i|O, \lambda)
$$&lt;/p&gt;
&lt;p&gt;可以通过前向概率和后向概率进行计算，推导如下：&lt;/p&gt;
&lt;p&gt;$$
\gamma_t(i) = P(i_t = q_i|O, \lambda) = \frac{P(i_t = q_i, O|\lambda)}{P(O|\lambda)}
$$&lt;/p&gt;
&lt;p&gt;又由前向概率和后向概率的定义可知：&lt;/p&gt;
&lt;p&gt;$$
\alpha_t(i) \beta_t(i) =  P(i_t = q_i, O|\lambda)
$$&lt;/p&gt;
&lt;p&gt;因此有：&lt;/p&gt;
&lt;p&gt;$$
\gamma_t(i) = \frac{P(i_t = q_i, O|\lambda)}{P(O|\lambda)} = \frac{P(i_t = q_i, O|\lambda)}{\sum_{j=1}^{N} P(i_t = q_j, O|\lambda)} = \frac{\alpha_t(i) \beta_t(i)}{\sum_{j=1}^{N} \alpha_t(j) \beta_t(j)}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给定模型参数 $\lambda$ 和观测 $O$ ，在时刻 $t$ 处于状态 $q_i$ 且在时刻 $t+1$ 处于状态 $q_j$ 的概率，记:&lt;/p&gt;
&lt;p&gt;$$
\xi_t(i, j) = P(i_t = q_i, i_{t+1} = q_j|O,\lambda)
$$&lt;/p&gt;
&lt;p&gt;可以通过前向后向概率进行计算，推导如下：&lt;/p&gt;
&lt;p&gt;$$
\xi_t(i, j) = \frac{P(i_t = q_i, i_{t+1} = q_j, O|\lambda)}{P(O|\lambda)} = \frac{P(i_t = q_i, i_{t+1} = q_j, O|\lambda)}{\sum_{i=1}^{N} \sum_{j=1}^{N} P(i_t = q_i, i_{t+1} = q_j, O|\lambda)}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;p&gt;$$
P(i_t = q_i, i_{t+1} = q_j, O|\lambda) = \alpha_t(i) a_{ij} b_{jo_{t+1}} \beta_{t+1}(j)
$$&lt;/p&gt;
&lt;p&gt;因此有：&lt;/p&gt;
&lt;p&gt;$$
\xi_t(i, j) = \frac{\alpha_t(i) a_{ij} b_{jo_{t+1}} \beta_{t+1}(j)}{\sum_{i=1}^{N} \sum_{j=1}^{N} \alpha_t(i) a_{ij} b_{jo_{t+1}} \beta_{t+1}(j)}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;学习问题&lt;/h2&gt;
&lt;p&gt;学习 Baum-Welch 算法需要用到 EM 算法的知识，如果不知道什么是 EM 算法可以到本系列的上一篇博客进行学习（直接看 EM 算法的部分即可）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Baum-Welch 算法本身也非常复杂，可以结合其他的博客辅助理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/u014688145/article/details/53046765&quot;&gt;隐马尔可夫模型之Baum-Welch算法详解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_44648285/article/details/146015483&quot;&gt;隐马尔可夫模型（HMM）三大基础问题之——学习问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/xmu_jupiter/article/details/50965039&quot;&gt;HMM的Baum-Welch算法和Viterbi算法公式推导细节&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://people.csail.mit.edu/stephentu/writeups/hmm-baum-welch-derivation.pdf&quot;&gt;Derivation of Baum-Welch Algorithm for Hidden Markov Models&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/688555596&quot;&gt;Python 机器学习 维特比算法和鲍姆-韦尔奇算法&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;监督学习方法&lt;/h3&gt;
&lt;p&gt;我们有 $N$ 个隐状态 $S = { S_1, S_2, \ldots, S_N }$ ，观测符号集合 $V = { v_1, v_2, \ldots, v_M }$ 。&lt;/p&gt;
&lt;p&gt;训练集中包含 $K$ 条样本序列，每条样本包含一个 &lt;strong&gt;状态序列&lt;/strong&gt; 和对应的 &lt;strong&gt;观测序列&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
I^{(k)} = (i_1^{(k)}, i_2^{(k)}, \ldots, i_{T_k}^{(k)}) \quad O^{(k)} = (o_1^{(k)}, o_2^{(k)}, \ldots, o_{T_k}^{(k)})
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始状态概率&lt;/strong&gt; 的极大似然估计为：&lt;/p&gt;
&lt;p&gt;$$
\pi_i = P(i_1 = S_1) = \frac{\sum_{k = 1}^{K} \mathbb{I} (i_1^{(k)} = S_i)}{K}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;统计所有序列的第一个状态是 $S_i$ 的频率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;状态转移概率&lt;/strong&gt; 的极大似然估计为：&lt;/p&gt;
&lt;p&gt;$$
a_{ij} = P(i_{t+1} = S_j|i_t = S_i) = \frac{\sum_{k=1}^{K} \sum_{t=1}^{T_k-1} \mathbb{I} (i_t^{(k)} = S_i, i_{t+1}^{(k)} = S_j)}{\sum_{k=1}^{K} \sum_{t=1}^{T_k-1} \mathbb{I} (i_t^{(k)} = S_i)}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从 $S_i$ 转移到 $S_j$ 的次数除以从 $S_i$ 转移出去的总次数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;观测（发射）概率&lt;/strong&gt; 的极大似然估计：&lt;/p&gt;
&lt;p&gt;$$
b_i(k) = P(O_t = v_k|i_t = S_i) = \frac{\sum_{k&lt;code&gt;=1}^{K} \sum_{t=1}^{T_{k&lt;/code&gt;}} \mathbb{I} (i_t^{k&lt;code&gt;} = S_i, o_t^{(k&lt;/code&gt;)} = v_k)}{\sum_{k&lt;code&gt;=1}^{K} \sum_{t=1}^{T_{k&lt;/code&gt;}} \mathbb{I} (i_t^{(k`)} = S_i)}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在状态 $S_i$ 下观测到符号 $v_k$ 的次数除以状态 $S_i$ 出现的总次数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;显然此训练数据中的状态序列数据通常是需要人工标注出来的，因此代价较高，所以非监督学习的方法更为实用。&lt;/p&gt;
&lt;h3&gt;Baum-Welch 算法&lt;/h3&gt;
&lt;p&gt;如果只有观测序列数据 $O = (o_1, o_2, \ldots, o_T)$ ，而没有状态序列数据 $I = (i_1, i_2, \ldots, i_T)$ ，那么隐马尔可夫模型就是一个含有隐藏变量的概率模型：&lt;/p&gt;
&lt;p&gt;$$
P(O|\lambda) = \sum_I P(O|I, \lambda)P(I|\lambda)
$$&lt;/p&gt;
&lt;p&gt;如果要对它进行参数估计，可以采用 &lt;strong&gt;EM 算法&lt;/strong&gt; 来实现，具体步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定完全数据的对数似然函数&lt;/p&gt;
&lt;p&gt;此时观测数据为 $O = (o_1, o_2, \ldots, o_T)$ ，未观测数据为 $I = (i_1, i_2, \ldots, i_T)$ ，则完全数据为 $(O, I) = (o_1, o_2, \ldots, o_T, i_1, i_2, \ldots, i_T)$ ，完全数据的对数似然函数为：&lt;/p&gt;
&lt;p&gt;$$
\log P(O, I|\lambda) = \log \left[ \pi_{i_1} \left( \prod_{t=1}^{T-1} b_{i_to_t}a_{i_ti_{t+1}} \right) b_{i_To_T} \right] = \log \pi_{i_1} + \sum_{t=1}^{T-1} \log a_{i_ti_{t+1}} + \sum_{t=1}^{T} \log b_{i_to_t}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EM 算法 E 步：求解 $Q$ 函数&lt;/p&gt;
&lt;p&gt;写出对完整数据的 &lt;strong&gt;条件期望对数似然函数&lt;/strong&gt; ：&lt;/p&gt;
&lt;p&gt;$$
Q(\lambda, \bar{\lambda}) = \sum_I P(I|O, \bar{\lambda}) \log P(O, I|\lambda)
$$&lt;/p&gt;
&lt;p&gt;其中 $\bar{\lambda}$ 是隐马尔可夫模型参数的当前估计值， $\lambda$ 是要极大化的隐马尔可夫模型参数。为了便于后续计算， $Q$ 函数还可以作如下恒等变形：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\begin{align*}
Q(\lambda, \bar{\lambda}) &amp;amp;= \sum_{I} P(I|O, \bar{\lambda}) \log P(O, I|\lambda) \
&amp;amp;= \sum_{I} \frac{P(I|O, \bar{\lambda})P(O|\bar{\lambda})}{P(O|\bar{\lambda})} \log P(O, I|\lambda) \
&amp;amp;= \sum_{I} \frac{P(O, I|\bar{\lambda})}{P(O|\bar{\lambda})} \log P(O, I|\lambda)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;由于接下来仅极大化 $\lambda$ ，所以 $P(O|\bar{\lambda})$ 可以看做常数项进行略去，所以 $Q$ 函数可以进一步化简：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\begin{aligned}
Q(\lambda, \bar{\lambda}) &amp;amp;= \sum_{I} P(O, I| \bar{\lambda}) \log P(O, I|\lambda) = \sum_{I} P(O, I| \bar{\lambda}) \left( \log \pi_{i_1} + \sum_{t=1}^{T-1} \log a_{i_t i_{t+1}} + \sum_{t=1}^{T} \log b_{i_t o_t} \right) \
&amp;amp;= \sum_{I} P(O, I| \bar{\lambda}) \log \pi_{i_1} + \sum_{I} P(O, I| \bar{\lambda}) \left( \sum_{t=1}^{T-1} \log a_{i_t i_{t+1}} \right) + \sum_{I} P(O, I| \bar{\lambda}) \left( \sum_{t=1}^{T} \log b_{i_t o_t} \right)
\end{aligned}
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;EM 算法 M 步：极大化 $Q$ 函数&lt;/p&gt;
&lt;p&gt;由于要极大化的三个参数在上式中单独地出现在每个项中，所以只需对各项分别极大化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;求解 &lt;strong&gt;初始状态概率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上述 $Q$ 函数的第一项可以写成：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\sum_I P(O, I|\bar{\lambda}) \log \pi_{i_1} &amp;amp;= \sum_{i=1}^{N} \log \pi_i \left[ \sum_{i_2, i_3, \ldots, i_T} P(O, i_1 = q_1, i_2, i_3, \ldots, i_T|\bar{\lambda}) \right] \
&amp;amp;= \sum_{i=1}^{N} \log \pi_i P(O, i_1 = q_1|\bar{\lambda}) = \sum_{i=1}^{N} \log \pi_i P(O, i_1 = q_i|\bar{\lambda})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由于 $\pi_i$ 需要满足约束 $\sum_{i=1}^{N} \pi_i = 1$ ，利用拉格朗日乘数法，写出拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
L(\pi_i, \eta) = \sum_{i=1}^N \log \pi_i P(O,i_1 = q_i|\bar{\lambda}) + \eta \left( \sum_{i=1}^N \pi_i - 1 \right)
$$&lt;/p&gt;
&lt;p&gt;对其关于 $\pi_i$ 偏导并令其结果为 0 可得:&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial}{\partial \pi_i} \left[ \sum_{i=1}^N \ln \pi_i P(O, i_1 = q_i | \bar{\lambda}) + \eta \left( \sum_{i=1}^N \pi_i - 1 \right) \right] = 0
$$&lt;/p&gt;
&lt;p&gt;$$
P(O, i_1 = q_i|\bar{\lambda}) + \eta \pi_i = 0
$$&lt;/p&gt;
&lt;p&gt;对上式关于 $i$ 求和可得：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{N} P(O, i_1 = q_i|\bar{\lambda}) + \sum_{i=1}^{N} \eta \pi_i = 0
$$&lt;/p&gt;
&lt;p&gt;$$
P(O|\bar{\lambda}) + \eta = 0
$$&lt;/p&gt;
&lt;p&gt;将 $\eta$ 代回原式可得：&lt;/p&gt;
&lt;p&gt;$$
P(O, i_1 = q_i|\bar{\lambda}) - P(O|\bar{\lambda}) \pi_i = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\pi_i = \frac{P(O, i_1 = q_i|\bar{\lambda})}{P(O|\bar{\lambda})} = \gamma_1(i)
$$&lt;/p&gt;
&lt;p&gt;其中 $\gamma$ 就是&lt;a href=&quot;#%E7%AE%97%E6%B3%95%E6%8E%A8%E5%B9%BF&quot;&gt;算法推广&lt;/a&gt;中求解的 $\gamma$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;求解 &lt;strong&gt;状态转移概率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上述 $Q$ 函数的第二项可以写成：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp;\sum_I P(O,I|\bar{\lambda}) \left( \sum_{t=1}^{T-1} \log a_{i_t i_{t+1}} \right) \
= &amp;amp;\sum_{t=1}^{T-1} \sum_{i=1}^N \sum_{j=1}^N \log a_{ij} \left[ \sum_{(i_1,\ldots,i_{t-1},i_{t+2},\ldots,i_T)} P(O, i_1, \ldots, i_t = q_i, i_{t+1} = q_j, \ldots, i_T|\bar{\lambda}) \right] \
= &amp;amp;\sum_{t=1}^{T-1} \sum_{i=1}^N \sum_{j=1}^N \log a_{ij} P(O,i_t = q_i,i_{t+1} = q_j | \bar{\lambda})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由于 $a_{ij}$ 满足约束 $\sum_{j=1}^{N} a_{ij} = 1$ ，同样利用拉格朗日乘数法，写出拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
L(a_{ij}, \eta) = \sum_{t=1}^{T-1} \sum_{i=1}^{N} \sum_{j=1}^{N} \log a_{ij} P(O, i_t = q_i, i_{t+1} = q_j|\bar{\lambda}) + \eta \left( \sum_{j=1}^{N} a_{ij} - 1 \right)
$$&lt;/p&gt;
&lt;p&gt;对其关于 $a_{ij}$ 偏导并令其结果为 0 可得:&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial}{\partial a_{ij}} \left[ \sum_{t=1}^{T-1} \sum_{i=1}^{N} \sum_{j=1}^{N} \log a_{ij} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda}) + \eta \left( \sum_{j=1}^{N} a_{ij} - 1 \right) \right] = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T-1} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda}) + \eta a_{ij} = 0
$$&lt;/p&gt;
&lt;p&gt;对上式关于 $j$ 求和可得：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=1}^{N} \sum_{t=1}^{T-1} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda}) + \sum_{j=1}^{N} \eta a_{ij} = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T-1} P(O, i_t = q_i|\bar{\lambda}) + \eta = 0
$$&lt;/p&gt;
&lt;p&gt;将 $\eta$ 代回原式可得：&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T-1} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda}) - \sum_{t=1}^{T-1} P(O, i_t = q_i | \bar{\lambda}) \cdot a_{ij} = 0
$$&lt;/p&gt;
&lt;p&gt;$$
a_{ij} = \frac{\sum_{t=1}^{T-1} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda})}{\sum_{t=1}^{T-1} P(O, i_t = q_i | \bar{\lambda})}
$$&lt;/p&gt;
&lt;p&gt;分子分母同时除以 $P(O|\bar{\lambda})$ 可得：&lt;/p&gt;
&lt;p&gt;$$
a_{ij} = \frac{\displaystyle\frac{\sum_{t=1}^{T-1} P(O, i_t = q_i, i_{t+1} = q_j | \bar{\lambda})}{P(O|\bar{\lambda})}}{\displaystyle\frac{\sum_{t=1}^{T-1} P(O, i_t = q_i | \bar{\lambda})}{P(O|\bar{\lambda})}} = \frac{\sum_{t=1}^{T-1} P(i_t = q_i, i_{t+1} = q_j | O, \bar{\lambda})}{\sum_{t=1}^{T-1} P(i_t = q_i | O, \bar{\lambda})}
$$&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;p&gt;$$
a_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i, j)}{\sum_{t=1}^{T-1} \gamma_t(i)}
$$&lt;/p&gt;
&lt;p&gt;其中 $\gamma$ 和 $\xi$ 就是&lt;a href=&quot;#%E7%AE%97%E6%B3%95%E6%8E%A8%E5%B9%BF&quot;&gt;算法推广&lt;/a&gt;中求解的 $\gamma$ 和 $\xi$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;求解 &lt;strong&gt;观测（发射）概率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上述 $Q$ 函数的第三项可以写成：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp;\sum_{I} P(O, I|\bar{\lambda}) \left( \sum_{t=1}^{T} \log b_{i_t o_t} \right) \
=&amp;amp; \sum_{t=1}^{T} \sum_{j=1}^{N} \log b_{j o_t} \left[ \sum_{i_1, \ldots, i_{t-1}, i_{t+1}, \ldots, i_T} P(O, i_1, \ldots, i_t = q_j, \ldots, i_T|\bar{\lambda}) \right] \
=&amp;amp; \sum_{t=1}^{T} \sum_{j=1}^{N} \log b_{j o_t} P(O, i_t = q_j|\bar{\lambda})
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由于 $b_{jk}$ 满足约束 $\sum_{k=1}^{M} b_{jk} = 1$ ，同样利用拉格朗日乘数法，写出拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
L(b_{jk}, \eta) = \sum_{t=1}^{T} \sum_{j=1}^{N} \ln b_{j o_t} P(O, i_t = q_j | \bar{\lambda}) + \eta \left( \sum_{k=1}^{M} b_{jk} - 1 \right)
$$&lt;/p&gt;
&lt;p&gt;对其关于 $b_{jk}$ 偏导并令其结果为 0 可得:&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial}{\partial b_{jk}} \left[ \sum_{t=1}^{T} \sum_{j=1}^{N} \ln b_{j o_t} P(O, i_t = q_j | \bar{\lambda}) + \eta \left( \sum_{k=1}^{M} b_{jk} - 1 \right) \right] = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \mathbb{I}(o_t = v_k) + \eta b_{jk} = 0
$$&lt;/p&gt;
&lt;p&gt;对上式关于 $k$ 求和可得：&lt;/p&gt;
&lt;p&gt;$$
\sum_{k=1}^{M} \sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \mathbb{I}(o_t = v_k) + \sum_{k=1}^{M} \eta b_{jk} = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T} P(O, i_t = q_j|\bar{\lambda}) + \eta = 0
$$&lt;/p&gt;
&lt;p&gt;将 $\eta$ 代回原式可得：&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \mathbb{I}(o_t = v_k) - \sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \cdot b_{jk} = 0
$$&lt;/p&gt;
&lt;p&gt;$$
b_{jk} = \frac{\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \mathbb{I}(o_t = v_k)}{\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda})}
$$&lt;/p&gt;
&lt;p&gt;分子分母同时除以 $P(O|\bar{\lambda})$ 可得：&lt;/p&gt;
&lt;p&gt;$$
b_{jk} = \frac{\displaystyle\frac{\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda}) \mathbb{I}(o_t = v_k)}{P(O|\bar{\lambda})}}{\displaystyle\frac{\sum_{t=1}^{T} P(O, i_t = q_j | \bar{\lambda})}{P(O|\bar{\lambda})}} = \frac{\sum_{t=1}^{T} P(i_t = q_j | O, \bar{\lambda}) \mathbb{I}(o_t = v_k)}{\sum_{t=1}^{T} P(i_t = q_j | O, \bar{\lambda})}
$$&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;p&gt;$$
b_{jk} = \frac{\sum_{t=1, o_t = v_k}^{T} \gamma_t(j)}{\sum_{t=1}^{T} \gamma_t(j)}
$$&lt;/p&gt;
&lt;p&gt;其中 $\gamma$ 就是&lt;a href=&quot;#%E7%AE%97%E6%B3%95%E6%8E%A8%E5%B9%BF&quot;&gt;算法推广&lt;/a&gt;中求解的 $\gamma$ 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;预测问题&lt;/h2&gt;
&lt;p&gt;Viterbi 算法也是一个动态规划算法，熟悉动态规划的读者理解起来会比较轻松。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果不了解什么是动态规划的话可以观看下列视频后再来看详细推导。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=596607341&amp;amp;bvid=BV1ZB4y1y7gC&amp;amp;cid=720556511&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=511741950&amp;amp;bvid=BV1kg411d7qk&amp;amp;cid=725097082&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;近似算法&lt;/h3&gt;
&lt;p&gt;在每个时刻 $t$ 选择在该时刻最有可能出现的状态 $i_t^&lt;em&gt;$ ，从而得到一个状态序列 $I^&lt;/em&gt; = (i_1^&lt;em&gt;, i_2^&lt;/em&gt;, \ldots, i_T^*)$ ，将它作为预测的结果。&lt;/p&gt;
&lt;p&gt;给定隐马尔可夫模型 $\lambda$ 和观测序列 $O$ ，在时刻 $t$ 处于状态 $q_i$ 的概率$\gamma_t(i)$ 为：&lt;/p&gt;
&lt;p&gt;$$
\gamma_t(i) = \frac{\alpha_t(i) \beta_t(i)}{\sum_{j=1}^{N} \alpha_t(j) \beta_t(j)}
$$&lt;/p&gt;
&lt;p&gt;在每一时刻 $t$ 最有可能的状态 $i_t^*$ 为：&lt;/p&gt;
&lt;p&gt;$$
i_t^* = \arg \max_{1 \leq i \leq N} \big[\gamma_t(i)\big] \quad t = 1, 2, \ldots, T
$$&lt;/p&gt;
&lt;p&gt;从而得到状态序列 $I^* = (i_1^&lt;em&gt;, i_2^&lt;/em&gt;, \ldots, i_T^*)$ 。&lt;/p&gt;
&lt;p&gt;近似算法的优点是计算简单，其缺点是 &lt;strong&gt;不能保证&lt;/strong&gt; 预测的状态序列整体是 &lt;strong&gt;最有可能&lt;/strong&gt; 的状态序列，因为预测的序列可能有实际不发生的部分，也即可能存在状态转移概率 $a_{i^&lt;em&gt;j^&lt;/em&gt;} = 0$ 的相邻状态 $i^&lt;em&gt;$ 和 $j^&lt;/em&gt;$ 出现。尽管如此，近似算法仍然是 &lt;strong&gt;有用的&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;Viterbi 算法&lt;/h3&gt;
&lt;p&gt;Viterbi 算法实际是用 &lt;strong&gt;动态规划&lt;/strong&gt; 解隐马尔可夫模型预测问题，即用动态规划求概率最大路径，这时一条路径对应着一个状态序列。具体算法如下：&lt;/p&gt;
&lt;p&gt;定义在时刻 定义在时刻 $t$ 状态为 $q_i$ 的所哟单个路径 $(i_1, i_2, \ldots, i_t)$ 中概率最大值为：&lt;/p&gt;
&lt;p&gt;$$
\delta_t(i) = \max_{i_1, i_2, \ldots, i_{t-1}} P(o_1, \ldots, o_t, i_1, \ldots, i_{t-1}, i_t = q_i) \quad i = 1, 2, \ldots, N
$$&lt;/p&gt;
&lt;p&gt;由此定义可推得：&lt;/p&gt;
&lt;p&gt;$$
\delta_1(i) = \pi_i b_{i o_1}
$$&lt;/p&gt;
&lt;p&gt;$$
\delta_2(i) = \max_{1 \leq j \leq N} \big[\delta_1(j) a_{ji}\big] b_{i o_2}
$$&lt;/p&gt;
&lt;p&gt;$$
\delta_3(i) = \max_{1 \leq j \leq N} \big[\delta_2(j) a_{ji}\big] b_{i o_3}
$$&lt;/p&gt;
&lt;p&gt;依次此类推可得如下递推公式：&lt;/p&gt;
&lt;p&gt;$$
\delta_t(i) = \max_{1 \leq j \leq N} \big[\delta_{t-1}(j) a_{ji}\big] b_{i o_t}
$$&lt;/p&gt;
&lt;p&gt;同时再定义在时刻 $t$ 状态为 $q_i$ 的所有单个路径 $(i_1, i_2, \ldots, i_t)$ 中概率最大的路径的第 $t-1$ 个结点为：&lt;/p&gt;
&lt;p&gt;$$
\Psi_t(i) = \arg \max_{1 \leq j \leq N} \delta_{t-1}(j) a_{ji}
$$&lt;/p&gt;
&lt;p&gt;令 $i_T^* = \arg \max_{1 \leq i \leq N} \delta_T(i)$ 可得：&lt;/p&gt;
&lt;p&gt;$$
i_{T-1}^* = \Psi_T(i_T^&lt;em&gt;), i_{T-2}^&lt;/em&gt; = \Psi_{T-1}(i_{T-1}^&lt;em&gt;), \ldots, i_1^&lt;/em&gt; = \Psi_2(i_2^*)
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;隐马尔可夫模型代码讲解&lt;/h1&gt;
&lt;p&gt;虽然 HMM 的代码比之前所有模型都要长，但其实就是一些简单的函数，原理已在上文中讲过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
rng = np.random.RandomState(0)
obs_seq = np.random.randint(0, 3, size=50)


def _normalize(arr, axis=None, eps=1e-12):
    s = arr.sum(axis=axis, keepdims=True)
    s = np.maximum(s, eps)
    return arr / s

def _logsumexp(a, axis=None):
    a_max = np.max(a, axis=axis, keepdims=True)
    res = a_max + np.log(np.sum(np.exp(a - a_max), axis=axis, keepdims=True))
    if axis is None:
        return res.squeeze()
    return res

class HMM:
    def __init__(self, n_states, n_obs, seed=None):
        rng = np.random.RandomState(seed)
        self.n_states = n_states
        self.n_obs = n_obs
        # 初始化参数 π, A, B
        self.pi = _normalize(rng.rand(n_states))
        self.A = _normalize(rng.rand(n_states, n_states), axis=1)
        self.B = _normalize(rng.rand(n_states, n_obs), axis=1)

    # 发射概率
    def _emission_logprob(self, obs):
        assert obs.dtype.kind in &apos;iu&apos;, &quot;Discrete observations must be integer dtype&quot;
        logB = np.log(self.B[:, obs].T + 1e-12)
        return logB

    # 前向算法
    def _forward_log(self, obs):
        logA = np.log(self.A + 1e-12)
        logpi = np.log(self.pi + 1e-12)
        logB = self._emission_logprob(obs)
        T, S = logB.shape
        alpha = np.zeros((T, S))
        alpha[0] = logpi + logB[0]
        for t in range(1, T):
            a = alpha[t - 1][:, None] + logA
            alpha[t] = _logsumexp(a, axis=0).ravel() + logB[t]
        return alpha

    # 后向算法
    def _backward_log(self, obs):
        logA = np.log(self.A + 1e-12)
        logB = self._emission_logprob(obs)
        T, S = logB.shape
        beta = np.zeros((T, S))
        beta[T - 1] = 0.0
        for t in range(T - 2, -1, -1):
            b = logA + (logB[t + 1] + beta[t + 1])[None, :]
            beta[t] = _logsumexp(b, axis=1).ravel()
        return beta

    # 前后向算法（得到 gamma）
    def forward_backward(self, obs):
        alpha = self._forward_log(obs)
        beta = self._backward_log(obs)
        loggamma = alpha + beta
        loggamma -= _logsumexp(loggamma, axis=1)
        return np.exp(loggamma)

    # 计算对数似然
    def score(self, obs):
        alpha = self._forward_log(obs)
        return float(_logsumexp(alpha[-1]))

    # Viterbi 算法
    def viterbi(self, obs):
        logA = np.log(self.A + 1e-12)
        logpi = np.log(self.pi + 1e-12)
        logB = self._emission_logprob(obs)
        T, S = logB.shape
        delta = np.zeros((T, S))
        psi = np.zeros((T, S), dtype=int)
        delta[0] = logpi + logB[0]
        for t in range(1, T):
            val = delta[t - 1][:, None] + logA
            psi[t] = np.argmax(val, axis=0)
            delta[t] = np.max(val, axis=0) + logB[t]
        states = np.zeros(T, dtype=int)
        states[T - 1] = np.argmax(delta[T - 1])
        for t in range(T - 2, -1, -1):
            states[t] = psi[t + 1, states[t + 1]]
        return states

    # Baum-Welch 算法（EM算法）
    def fit(self, sequences, max_iter=100, tol=1e-4, verbose=False):
        prev_ll = None
        for it in range(max_iter):
            pi_count = np.zeros(self.n_states)
            A_count = np.zeros((self.n_states, self.n_states))
            B_count = np.zeros((self.n_states, self.n_obs))
            total_ll = 0.0
            # E-step: compute posteriors of hidden states
            for obs in sequences:
                T = len(obs)
                alpha = self._forward_log(obs)
                beta = self._backward_log(obs)
                loggamma = alpha + beta
                loggamma -= _logsumexp(loggamma, axis=1)
                gamma = np.exp(loggamma)
                total_ll += _logsumexp(alpha[-1])
                logA = np.log(self.A + 1e-12)
                logB = self._emission_logprob(obs)
                xi_sum = np.zeros((self.n_states, self.n_states))
                for t in range(T - 1):
                    l = alpha[t][:, None] + logA + (logB[t + 1] + beta[t + 1])[None, :]
                    l -= _logsumexp(l)
                    xi_sum += np.exp(l)
                pi_count += gamma[0]
                A_count += xi_sum
                for t in range(T):
                    B_count[:, obs[t]] += gamma[t]
            # M-step: update parameters
            self.pi = _normalize(pi_count)
            self.A = _normalize(A_count, axis=1)
            self.B = _normalize(B_count, axis=1)
            total_ll = float(total_ll)
            if verbose:
                print(f&quot;Iter {it+1}: log-likelihood = {total_ll:.6f}&quot;)
            if prev_ll is not None and abs(total_ll - prev_ll) &amp;lt; tol:
                break
            prev_ll = total_ll
        return self


# 执行代码
if __name__ == &apos;__main__&apos;:
    model = HMM(n_states=2, n_obs=3, seed=0)
    model.fit([obs_seq], max_iter=50, verbose=True)

    print(&apos;Trained pi:&apos;, model.pi)
    print(&apos;Trained A :&apos;, model.A)
    print(&apos;Trained B :&apos;, model.B)
    print(&apos;Viterbi   :&apos;, model.viterbi(obs_seq)[:20])
    print(&apos;Log-lik   :&apos;, model.score(obs_seq))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 计算问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#%E8%AE%A1%E7%AE%97%E9%97%AE%E9%A2%98&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 发射概率
def _emission_logprob(self, obs):
    assert obs.dtype.kind in &apos;iu&apos;, &quot;Discrete observations must be integer dtype&quot;
    logB = np.log(self.B[:, obs].T + 1e-12)
    return logB

# 前向算法
def _forward_log(self, obs):
    logA = np.log(self.A + 1e-12)
    logpi = np.log(self.pi + 1e-12)
    logB = self._emission_logprob(obs)
    T, S = logB.shape
    alpha = np.zeros((T, S))
    alpha[0] = logpi + logB[0]
    for t in range(1, T):
        a = alpha[t - 1][:, None] + logA
        alpha[t] = _logsumexp(a, axis=0).ravel() + logB[t]
    return alpha

# 后向算法
def _backward_log(self, obs):
    logA = np.log(self.A + 1e-12)
    logB = self._emission_logprob(obs)
    T, S = logB.shape
    beta = np.zeros((T, S))
    beta[T - 1] = 0.0
    for t in range(T - 2, -1, -1):
        b = logA + (logB[t + 1] + beta[t + 1])[None, :]
        beta[t] = _logsumexp(b, axis=1).ravel()
    return beta

# 前后向算法（得到 gamma）
def forward_backward(self, obs):
    alpha = self._forward_log(obs)
    beta = self._backward_log(obs)
    loggamma = alpha + beta
    loggamma -= _logsumexp(loggamma, axis=1)
    return np.exp(loggamma)

# 计算对数似然
def score(self, obs):
    alpha = self._forward_log(obs)
    return float(_logsumexp(alpha[-1]))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 学习问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#baum-welch-%E7%AE%97%E6%B3%95&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fit(self, sequences, max_iter=100, tol=1e-4, verbose=False):
    prev_ll = None
    for it in range(max_iter):
        pi_count = np.zeros(self.n_states)
        A_count = np.zeros((self.n_states, self.n_states))
        B_count = np.zeros((self.n_states, self.n_obs))
        total_ll = 0.0
        # E-step: compute posteriors of hidden states
        for obs in sequences:
            T = len(obs)
            alpha = self._forward_log(obs)
            beta = self._backward_log(obs)
            loggamma = alpha + beta
            loggamma -= _logsumexp(loggamma, axis=1)
            gamma = np.exp(loggamma)
            total_ll += _logsumexp(alpha[-1])
            logA = np.log(self.A + 1e-12)
            logB = self._emission_logprob(obs)
            xi_sum = np.zeros((self.n_states, self.n_states))
            for t in range(T - 1):
                l = alpha[t][:, None] + logA + (logB[t + 1] + beta[t + 1])[None, :]
                l -= _logsumexp(l)
                xi_sum += np.exp(l)
            pi_count += gamma[0]
            A_count += xi_sum
            for t in range(T):
                B_count[:, obs[t]] += gamma[t]
        # M-step: update parameters
        self.pi = _normalize(pi_count)
        self.A = _normalize(A_count, axis=1)
        self.B = _normalize(B_count, axis=1)
        total_ll = float(total_ll)
        if verbose:
            print(f&quot;Iter {it+1}: log-likelihood = {total_ll:.6f}&quot;)
        if prev_ll is not None and abs(total_ll - prev_ll) &amp;lt; tol:
            break
        prev_ll = total_ll
    return self
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 预测问题&lt;/h2&gt;
&lt;p&gt;这个部分的代码可以观看&lt;a href=&quot;#viterbi-%E7%AE%97%E6%B3%95&quot;&gt;上面的讲解&lt;/a&gt;对照学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def viterbi(self, obs):
    logA = np.log(self.A + 1e-12)
    logpi = np.log(self.pi + 1e-12)
    logB = self._emission_logprob(obs)
    T, S = logB.shape
    delta = np.zeros((T, S))
    psi = np.zeros((T, S), dtype=int)
    delta[0] = logpi + logB[0]
    for t in range(1, T):
        val = delta[t - 1][:, None] + logA
        psi[t] = np.argmax(val, axis=0)
        delta[t] = np.max(val, axis=0) + logB[t]
    states = np.zeros(T, dtype=int)
    states[T - 1] = np.argmax(delta[T - 1])
    for t in range(T - 2, -1, -1):
        states[t] = psi[t + 1, states[t + 1]]
    return states
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;HMM 中用到的 BW 算法是特殊的 EM 算法，它究竟特殊在哪里？&lt;/p&gt;
&lt;p&gt;Baum–Welch 算法是 &lt;strong&gt;EM 算法的一个特例&lt;/strong&gt; ，它特殊的地方在于它的 &lt;strong&gt;隐变量结构&lt;/strong&gt; 和 &lt;strong&gt;E 步的计算方式&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;特殊的隐变量结构 ———— 马尔可夫链&lt;/p&gt;
&lt;p&gt;普通 EM 假设隐藏变量是独立的或结构简单的，而 HMM 的隐藏变量 $I$ 是一个马尔可夫链。它的依赖关系是：&lt;/p&gt;
&lt;p&gt;$$
P(I) = P(i_1) \prod_{t=2}^{T} P(i_t \mid i_{t-1})
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;E 步可解析计算 ———— 不用采样或者积分&lt;/p&gt;
&lt;p&gt;在一般 EM 中，E步往往要对所有隐变量求期望，复杂度极高。但在 HMM 中，隐状态间满足 &lt;strong&gt;马尔可夫性&lt;/strong&gt; ，因此可以用 &lt;strong&gt;前向-后向算法&lt;/strong&gt; 高效求解期望。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Cnoized/p/18916857&quot;&gt;机器学习-12-隐马尔可夫模型HMM&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/28412002248&quot;&gt;隐马尔可夫模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/xiang_gina/article/details/148400775&quot;&gt;马尔可夫模型 and 隐马尔可夫模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://allenwind.github.io/blog/7681/&quot;&gt;概率图模型系列（3）：隐马尔可夫模型（HMM）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.mselab.cn/media/files/B03.%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E6%A8%A1%E5%9E%8B.pdf&quot;&gt;马尔可夫模型及其应用&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1893730053705138991&quot;&gt;机器学习：隐马尔可夫模型(HMM)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/m0_53700832/article/details/140442722&quot;&gt;【机器学习】马尔可夫模型与隐马尔科夫模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sm1les.com/2019/04/10/hidden-markov-model/&quot;&gt;隐马尔可夫模型（HMM）及其三个基本问题&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第六节：高斯混合模型</title><link>https://xingguang641.com/posts/gaussian-mixture-module/gaussian-mixture-module/</link><guid isPermaLink="true">https://xingguang641.com/posts/gaussian-mixture-module/gaussian-mixture-module/</guid><description>介绍机器学习常见的模型</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;高斯混合基本原理&lt;/h1&gt;
&lt;p&gt;前面我们讲到了诸多的 &lt;strong&gt;分类算法（Classification Algorithm）&lt;/strong&gt;，虽然它们的数学原理差别很大，但它们其实都属于 &lt;strong&gt;有监督学习（Supervised Learning）&lt;/strong&gt; 这一个类别当中。也就是说：它们在训练过程中需要数据拥有自己对应的标签。&lt;/p&gt;
&lt;p&gt;而我们今天要讲的 &lt;strong&gt;高斯混合模型&lt;/strong&gt;（Gaussian Mixture Model，简称 GMM），它是一个 &lt;strong&gt;聚类算法（Clustering Algorithm）&lt;/strong&gt;，而聚类算法不同于分类算法，它们属于 &lt;strong&gt;无监督学习（Unsupervised Learning）&lt;/strong&gt; 这一类别。也就是说：高斯混合模型不要求数据有自己的标签，高斯混合模型会自动地将数据分为不同的类别。&lt;/p&gt;
&lt;p&gt;我们来思考一个简单的问题：如果给你一些数据，你想用什么模型去拟合这些数据的分布呢？可能很多人的第一想法就是用正态分布去拟合，毕竟正态分布是生活中最为常见的分布。其实这个想法是正确的，但仍有缺陷：虽然正态分布是最常见的分布，但现实世界太过于复杂，我们不能确保一个正态分布就能拟合出所有的数据集。对此，我们可以试着想一想：如果用多个正态分布去拟合，效果会不会更好？没错，这正是高斯混合模型的出发点。&lt;/p&gt;
&lt;h2&gt;高斯加权混合&lt;/h2&gt;
&lt;p&gt;为了解决高斯模型的单峰性的问题，我们引入多个高斯模型的加权平均来拟合多峰数据：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \sum_{k=1}^K \alpha_k \mathcal{N}(\mu_k, \Sigma_k)
$$&lt;/p&gt;
&lt;p&gt;由于我们只能观察到每个样本 $x$ 的信息，而无法了解每个样本究竟属于哪个高斯分布，因此我们可以引入一个隐变量 $z$（ $z = k$ 表示样本属于第 $K$ 个高斯分布）来辅助我们的推导：&lt;/p&gt;
&lt;p&gt;$$
P(z = i) = p_i \quad \sum_{i=1}^{k} P(z = i) = 1
$$&lt;/p&gt;
&lt;p&gt;于是 $P(x)$ 可以写成：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \sum_z P(x,z) = \sum_{k=1}^K P(x,z=k) = \sum_{k=1}^K P(z=k) P(x|z=k)
$$&lt;/p&gt;
&lt;p&gt;最后可以得到：&lt;/p&gt;
&lt;p&gt;$$
P(x) = \sum_{k=1}^K p_k \mathcal{N}(x|\mu_k,\Sigma_k)
$$&lt;/p&gt;
&lt;p&gt;值得注意的是：高斯混合模型并 &lt;strong&gt;不在意&lt;/strong&gt; 每个数据点究竟属于哪个类别（只是推导过程关注于单个数据点）。它想要做的事情是让多个高斯模型去拟合整个数据集，从而去预测新数据属于哪哪个高斯分布。另外，高斯混合模型也 &lt;strong&gt;不能确定&lt;/strong&gt; 究竟要用多少个高斯云（即高斯分布的图像）去拟合图像，因此要自己设置初始值 $K$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cgaussian-mixture-module%5C%E9%AB%98%E6%96%AF%E6%B7%B7%E5%90%88%E6%A8%A1%E5%9E%8B1.jpg&quot; alt=&quot;高斯混合模型图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;梯度下降局限&lt;/h2&gt;
&lt;p&gt;写出高斯混合模型的对数似然函数：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
L(\theta) &amp;amp;= \sum_{i=1}^{N} \log P(x_i) = \sum_{i=1}^{N} \log \sum_{k=1}^{K} p_k \mathcal{N}(x_i | \mu_k, \Sigma_k)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;其中 $\theta = {p_1, p_2, \dots, p_K, \mu_1, \mu_2, \dots, \mu_K, \Sigma_1, \Sigma_2, \dots, \Sigma_K}$ 。&lt;/p&gt;
&lt;p&gt;对这个表达式直接通过求导，由于连加号的存在，会无法得到解析解。因此我们无法直接根据极大似然估计的原理对这个式子使用常见的梯度下降算法（这里哪怕用似然函数求导也无法得到解析解）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;EM 迭代算法&lt;/h1&gt;
&lt;p&gt;由于无法直接对含有隐变量的似然函数求导，所以梯度下降无法求解出 GMM 的极大似然估计。对此我们引入一个专门解决此类问题的算法：EM 算法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cgaussian-mixture-module%5CEM%E7%AE%97%E6%B3%951.jpg&quot; alt=&quot;高斯混合模型图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;证据下界&lt;/h2&gt;
&lt;p&gt;我们可以先假设 $Z$ 服从的分布为 $Z \sim q(Z | \theta)$ ，于是有：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log P(X | \theta) &amp;amp;= \log P(X, Z | \theta) - \log P(Z | X, \theta) \
&amp;amp;= \log \frac{P(X, Z | \theta)}{q(Z | \theta)} - \log \frac{P(Z | X, \theta)}{q(Z | \theta)}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;两边同时关于 $Z \sim q(Z | \theta)$ 同时计算期望：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\log P(X | \theta) &amp;amp;= \sum_{Z} q(Z | \theta) \log \frac{P(X, Z | \theta)}{q(Z | \theta)} - \sum_{Z} q(Z | \theta) \log \frac{P(Z | X, \theta)}{q(Z | \theta)} \
&amp;amp;= \mathbb{E}&lt;em&gt;{Z \sim P(Z|X,\theta^{(t)})} \Big[ \log P(X, Z | \theta) \Big] - \sum&lt;/em&gt;{Z} q(Z | \theta) \log q(Z | \theta) + \operatorname{KL}\Big(q(Z | \theta) \parallel P(Z | X, \theta)\Big) \
&amp;amp;= \mathbb{E}_{Z \sim P(Z|X,\theta^{(t)})} \Big[\log P(X, Z | \theta) \Big] + H(q(Z | \theta)) + \operatorname{KL}\Big(q(Z | \theta) \parallel P(Z | X, \theta)\Big) \
&amp;amp;= ELBO(q, \theta | X) + \operatorname{KL}\Big(q(Z | \theta) \parallel P(Z | X, \theta)\Big)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;由于 KL 散度始终大于 0 ，因此 &lt;strong&gt;ELBO&lt;/strong&gt;（全称为 Evidence Lower Bound Optimization，中文译名为 &lt;strong&gt;证据下界&lt;/strong&gt; ）是 $L(\theta)$ 的一个下界（至于什么是 KL 散度可以参考下面这个视频）。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114558102410096&amp;amp;bvid=BV1r6jHzpE1J&amp;amp;cid=30166354742&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;流程介绍&lt;/h2&gt;
&lt;p&gt;EM 算法本质上是通过最大化 ELBO 来间接最大化对数似然函数。具体步骤分为 E-step 和 M-step。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寻找使得 KL 散度最小的 $q^{(t+1)}(Z) = P\left( Z | X, \theta^{(t)} \right)$ ，使得 ELBO 进一步逼近 $L(\theta)$&lt;/li&gt;
&lt;li&gt;寻找 $ELBO(\theta | q^{(t+1)}, X)$ 的极大值点作为新参数 $\theta^{(t+1)}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者交替迭代，最终收敛到局部最优解。&lt;/p&gt;
&lt;h3&gt;E-step&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;E-step 的核心是求解对数似然函数的期望&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将上面的式子稍微变形：&lt;/p&gt;
&lt;p&gt;$$
L(\theta) - \text{ELBO}(q,\theta | X) = \text{KL}\Big(q \parallel P(Z | X,\theta)\Big)
$$&lt;/p&gt;
&lt;p&gt;要使 ELBO 逼近 $L(\theta)$ ，就要让 KL 散度最小，先通过当前参数 $\theta^{(t)}$ 估计 $q^{(t+1)}$ ，得 $q^{(t+1)}(Z) = P\left(Z | X, \theta^{(t)}\right)$ ，于是有：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
L(\theta) &amp;amp;= \log P(X | \theta) = \mathbb{E}_{Z \sim P(Z | X, \theta^{(t)})} \Big[ \log P(X | \theta) \Big] \
&amp;amp;= ELBO(\theta | q^{(t+1)}, X) + \text{KL}\Big(P(Z | X, \theta^{(t)}) \parallel P(Z | X, \theta)\Big)
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;这里我们将 $ELBO(\theta | q^{(t+1)}, X)$ 记为 $Q(\theta | \theta^{(t)})$ ：&lt;/p&gt;
&lt;p&gt;$$
Q(\theta | \theta^{(t)}) = \mathbb{E}_{Z \sim P(Z | X,\theta^{(t)})} \Big[ \log P(X,Z | \theta) \Big] + H(P(Z | X,\theta^{(t)}))
$$&lt;/p&gt;
&lt;h3&gt;M-step&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;M-step 的核心是最大化对数似然函数的期望&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于信息熵为常数项，因此最大化 $Q(\theta | \theta^{(t)})$ 等价于将对数似然 $\log P(X, Z | \theta)$ 的期望最大化：&lt;/p&gt;
&lt;p&gt;$$
\theta^{(t+1)} = \arg \max_{\theta} Q(\theta | \theta^{(t)}) = \arg \max_{\theta} \mathbb{E}_{Z \sim P(Z|X,\theta^{(t)})} \Big[ \log P(X, Z | \theta) \Big]
$$&lt;/p&gt;
&lt;h2&gt;理论推导&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下推导部分参考自该视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=1400527903&amp;amp;bvid=BV1Q6421u7qb&amp;amp;cid=1448856301&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;E-step&lt;/h3&gt;
&lt;p&gt;我们先用 &lt;strong&gt;琴声不等式（Jensen&apos;s inequality）&lt;/strong&gt; 放缩的方式求解出 ELBO：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
L(\theta^{(t)}) &amp;amp;= \sum_{X} \log P(X | \theta^{(t)}) = \sum_{X} \log \left[ \sum_{Z} P(X,Z | \theta^{(t)}) \right] \
&amp;amp;= \sum_{X} \log \left[ \sum_{Z} q^{(t+1)}(Z), \frac{P(X,Z | \theta^{(t)})}{q^{(t+1)}(Z)} \right] = \sum_{X} \log \mathbb{E}&lt;em&gt;{Z\sim q^{(t+1)}(Z)} \Big[ \frac{P(X,Z | \theta^{(t)})}{q^{(t+1)}(Z)} \Big] \
&amp;amp;\ge \sum&lt;/em&gt;{X} \mathbb{E}&lt;em&gt;{Z\sim q^{(t+1)}(Z)} \Big[ \log \frac{P(X,Z | \theta^{(t)})}{q^{(t+1)}(Z)} \Big] = \sum&lt;/em&gt;{X}\sum_{Z} q^{(t+1)}(Z) \log \frac{P(X,Z | \theta^{(t)})}{q^{(t+1)}(Z)}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;根据琴声不等式的取等条件我们可知：&lt;/p&gt;
&lt;p&gt;$$
\frac{P(X, Z | \theta^{(t)})}{q^{(t+1)}(Z)} = C \quad \text{where } \sum_{Z} q^{(t+1)}(Z) = 1
$$&lt;/p&gt;
&lt;p&gt;将 $q^{(t+1)}(Z)$ 乘到等式的右侧可得：&lt;/p&gt;
&lt;p&gt;$$
P(X, Z | \theta^{(t)}) = C \cdot q^{(t+1)}(Z)
$$&lt;/p&gt;
&lt;p&gt;因为 $q^{(t+1)}(Z)$ 对变量 $Z$ 的积分为 1，因此我们将两边同时对 $Z$ 进行积分：&lt;/p&gt;
&lt;p&gt;$$
\sum_{Z} P(X, Z | \theta^{(t)}) = \sum_{Z} C \cdot q^{(t+1)}(Z) = C \sum_{Z} q^{(t+1)}(Z) = C
$$&lt;/p&gt;
&lt;p&gt;将上式重新代入琴声不等式的取等条件中：&lt;/p&gt;
&lt;p&gt;$$
q^{(t+1)}(Z) = \frac{P(X, Z | \theta^{(t)})}{\sum_{Z} P(X, Z | \theta^{(t)})} = \frac{P(X, Z | \theta^{(t)})}{P(X | \theta^{(t)})} = P(Z | X, \theta^{(t)})
$$&lt;/p&gt;
&lt;p&gt;于是我们就轻松地求解出 E-step 中的 $q^{(t+1)}(Z)$ 了。&lt;/p&gt;
&lt;h3&gt;M-step&lt;/h3&gt;
&lt;p&gt;由于不等式已经取等，因此有：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
L(\theta^{(t)}) &amp;amp;= \sum_{X} \sum_{Z} q^{(t+1)}(Z) \log \left[ \frac{P(X, Z | \theta^{(t)})}{q^{(t+1)}(Z)} \right] \
&amp;amp;= \sum_{X, Z} q^{(t+1)}(Z) \log P(X, Z | \theta^{(t)}) - \sum_{X, Z} q^{(t+1)}(Z) \log q^{(t+1)}(Z) \
&amp;amp;= \mathbb{E}&lt;em&gt;{Z\sim q^{(t+1)}(Z)} \Big[ L(X, Z | \theta^{(t)}) \Big] - \sum&lt;/em&gt;{X, Z} P(Z | X, \theta^{(t)}) \log P(Z | X, \theta^{(t)})
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;由于后面的信息熵为常数，所以 $\theta$ 的极大似然估计为：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\theta^{(t+1)} &amp;amp;= \arg \max_{\theta} \sum_{X} \sum_{Z} q^{(t+1)}(Z) \log \left[ \frac{P(X, Z | \theta)}{q^{(t+1)}(Z)} \right] \
&amp;amp;= \arg \max_{\theta} \mathbb{E}_{Z\sim P(Z | X, \theta)} \Big[ \log P(X, Z | \theta^{(t)}) \Big]
\end{align*}
$$&lt;/p&gt;
&lt;h2&gt;收敛证明&lt;/h2&gt;
&lt;p&gt;EM 算法的流程并不复杂，但是还有一个很重要的问题需要我们思考：EM 算法收敛吗？如果 EM 算法无法正常收敛，那么这个算法的过程无论多么精美都没用。就让我们再证明一下 EM 算法的收敛性吧。&lt;/p&gt;
&lt;p&gt;根据单调有界原理，如果数列单调递增且有界，那么该数列收敛。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先我们来看看有界性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;定义似然函数：&lt;/p&gt;
&lt;p&gt;$$
L(\theta) = \sum_{X} \log P(X | \theta)
$$&lt;/p&gt;
&lt;p&gt;由于概率值有界，而有界函数的有限次线性组合仍然有界，$L(\theta)$ 有界。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后我们来看看单调性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不妨设函数 $F(q, \theta)$ ：&lt;/p&gt;
&lt;p&gt;$$
F(q, \theta) = \sum_{X}\sum_{Z} q(Z) \log \frac{P(X,Z | \theta)}{q(Z)}
$$&lt;/p&gt;
&lt;p&gt;其中 $q(Z)$ 可以是任意分布。&lt;/p&gt;
&lt;p&gt;在 EM 算法执行之前，根据琴声不等式，对于任意的 $q$ ，有下列关系：&lt;/p&gt;
&lt;p&gt;$$
L(\theta^{(t)}) \geq F(q, \theta^{(t)})
$$&lt;/p&gt;
&lt;p&gt;经过 E-step 的迭代后，因为 E-step 的目的就是让琴声不等式取等，所以有：&lt;/p&gt;
&lt;p&gt;$$
L(\theta^{(t)}) = F(q^{(t+1)}, \theta^{(t)})
$$&lt;/p&gt;
&lt;p&gt;因为 M-step 的目标如下：&lt;/p&gt;
&lt;p&gt;$$
\theta^{(t+1)} = \arg \max_{\theta} F(q^{(t+1)}, \theta)
$$&lt;/p&gt;
&lt;p&gt;显然有下列关系：&lt;/p&gt;
&lt;p&gt;$$
F(q^{(t+1)}, \theta^{(t+1)}) \geq F(q^{(t+1)}, \theta^{(t)})
$$&lt;/p&gt;
&lt;p&gt;回到最上面的关系，令 $q = q^{(t+1)}$&lt;/p&gt;
&lt;p&gt;$$
L(\theta^{(t+1)}) \geq F(q^{(t+1)}, \theta^{(t+1)})
$$&lt;/p&gt;
&lt;p&gt;将上面所有步骤组成不等式链可得：&lt;/p&gt;
&lt;p&gt;$$
L(\theta^{(t+1)}) \ge F(q^{(t+1)}, \theta^{(t+1)}) \ge F(q^{(t+1)}, \theta^{(t)}) = L(\theta^{(t)})
$$&lt;/p&gt;
&lt;p&gt;因此函数 $L(\theta)$ 具有单调性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;高斯混合代码实现&lt;/h1&gt;
&lt;p&gt;准备了这么多，终于可以来看一下 GMM 的代码了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
np.random.seed(0)
X1 = np.random.multivariate_normal([0,0], [[1,0],[0,1]], 100)
X2 = np.random.multivariate_normal([5,5], [[1,0],[0,1]], 100)
X = np.vstack([X1, X2])


# 计算多维高斯概率密度函数
def gaussian_pdf(x, mean, cov):
    D = x.shape[0]
    cov_det = np.linalg.det(cov)
    cov_inv = np.linalg.inv(cov)
    norm_const = 1.0 / np.sqrt((2 * np.pi)**D * cov_det)
    diff = x - mean
    return norm_const * np.exp(-0.5 * diff.T @ cov_inv @ diff)

class GMM:
    def __init__(self, n_components, tol=1e-6, max_iter=100):
        self.K = n_components
        self.tol = tol
        self.max_iter = max_iter

    def fit(self, X):
        # 初始化参数
        N, _ = X.shape
        self.p = np.ones(self.K) / self.K
        self.mu = X[np.random.choice(N, self.K, replace=False)]
        self.Sigma = np.array([np.cov(X, rowvar=False)] * self.K)

        log_likelihood_old = 0
        for _ in range(self.max_iter):
            # E-step
            gamma = np.zeros((N, self.K))
            for i in range(N):
                for k in range(self.K):
                    gamma[i, k] = self.p[k] * gaussian_pdf(X[i], self.mu[k], self.Sigma[k])
                gamma[i, :] /= np.sum(gamma[i, :])

            # M-step
            N_k = np.sum(gamma, axis=0)
            self.p = N_k / N
            self.mu = (gamma.T @ X) / N_k[:, np.newaxis]
            for k in range(self.K):
                diff = X - self.mu[k]
                self.Sigma[k] = (gamma[:, k][:, np.newaxis] * diff).T @ diff / N_k[k]

            # 计算对数似然函数判断是否收敛
            log_likelihood = 0
            for i in range(N):
                temp = 0
                for k in range(self.K):
                    temp += self.p[k] * gaussian_pdf(X[i], self.mu[k], self.Sigma[k])
                log_likelihood += np.log(temp)

            if np.abs(log_likelihood - log_likelihood_old) &amp;lt; self.tol:
                break
            log_likelihood_old = log_likelihood

        return self

    # 软预测函数
    def predict_proba(self, X):
        N = X.shape[0]
        gamma = np.zeros((N, self.K))
        for i in range(N):
            for k in range(self.K):
                gamma[i, k] = self.p[k] * gaussian_pdf(X[i], self.mu[k], self.Sigma[k])
            gamma[i, :] /= np.sum(gamma[i, :])
        return gamma

    # 硬预测函数
    def predict(self, X):
        return np.argmax(self.predict_proba(X), axis=1)


# 执行代码
if __name__ == &quot;__main__&quot;:
    gmm = GMM(n_components=2)
    gmm.fit(X)

    labels = gmm.predict(X)
    print(&quot;混合系数 p:&quot;, gmm.p)
    print(&quot;均值 mu:&quot;, gmm.mu)
    print(&quot;协方差 Sigma:&quot;, gmm.Sigma)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;E-step&lt;/h2&gt;
&lt;p&gt;下面给出 E-step 的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gamma = np.zeros((N, self.K))
for i in range(N):
    for k in range(self.K):
        gamma[i, k] = self.p[k] * gaussian_pdf(X[i], self.mu[k], self.Sigma[k])
    gamma[i, :] /= np.sum(gamma[i, :])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;E-step 的目标是计算每个数据点属于每个分量的 &lt;strong&gt;后验概率&lt;/strong&gt; ，因此有：&lt;/p&gt;
&lt;p&gt;$$
\gamma_{ik} = P(z_{ik} = 1 \mid x_i, \theta^{(t)}) = \frac{p_k^{(t)} , \mathcal{N}(x_i \mid \mu_k^{(t)}, \Sigma_k^{(t)})}{\sum_{j=1}^K p_j^{(t)} , \mathcal{N}(x_i \mid \mu_j^{(t)}, \Sigma_j^{(t)})}
$$&lt;/p&gt;
&lt;p&gt;这里 $\gamma_{ik}$ 通常称为 &lt;strong&gt;responsibility&lt;/strong&gt; ，表示第 $K$ 个高斯对 $x_i$ 的责任。&lt;/p&gt;
&lt;h2&gt;M-step&lt;/h2&gt;
&lt;p&gt;下面给出 M-step 的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N_k = np.sum(gamma, axis=0)
self.p = N_k / N
self.mu = (gamma.T @ X) / N_k[:, np.newaxis]
for k in range(self.K):
    diff = X - self.mu[k]
    self.Sigma[k] = (gamma[:, k][:, np.newaxis] * diff).T @ diff / N_k[k]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;M-step 的目的则是最大化 &lt;strong&gt;期望的完整数据对数似然&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
Q(\theta \mid \theta^{(t)}) = \sum_{i=1}^N \sum_{k=1}^K \gamma_{ik} , \log \big( p_k , \mathcal{N}(x_i \mid \mu_k, \Sigma_k) \big)
$$&lt;/p&gt;
&lt;p&gt;将似然函数对每个参数分别求偏导可得 &lt;strong&gt;混合系数$p_k$&lt;/strong&gt; 、&lt;strong&gt;均值$\mu_k$&lt;/strong&gt; 和 &lt;strong&gt;协方差$Sigma_k$&lt;/strong&gt; 的更新公式：&lt;/p&gt;
&lt;p&gt;$$
p_k^{(t+1)} = \frac{1}{N} \sum_{i=1}^N \gamma_{ik} \quad \mu_k^{(t+1)} = \frac{\sum_{i=1}^N \gamma_{ik} x_i}{\sum_{i=1}^N \gamma_{ik}}
$$&lt;/p&gt;
&lt;p&gt;$$
\Sigma_k^{(t+1)} = \frac{\sum_{i=1}^N \left[ \gamma_{ik} (x_i - \mu_k^{(t+1)}) \right]^{\rm T} (x_i - \mu_k^{(t+1)})}{\sum_{i=1}^N \gamma_{ik}}
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;h2&gt;EM 迭代算法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jaredddddd.github.io/2024/01/01/EM/&quot;&gt;EM算法的理解和详细推导&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/365641813&quot;&gt;深入理解EM算法（ELBO+KL形式）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2501_90186640/article/details/147234092&quot;&gt;深入剖析EM算法：原理、推导与应用&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://luyiyun.github.io/2020/12/08/methods/methods-em/&quot;&gt;EM算法详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/qizhou/p/13100817.html&quot;&gt;EM（最大期望）算法推导、GMM的应用与代码实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;高斯混合模型&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/30483076&quot;&gt;高斯混合模型（GMM）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Mixture_model#Gaussian_mixture_model&quot;&gt;【维基百科】高斯混合模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/conpi/p/18956198&quot;&gt;高斯混合模型 GMM计算方法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/Cnoized/p/18897547&quot;&gt;机器学习-09-高斯混合模型GMM&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/619191372&quot;&gt;GMM：高斯混合模型原理实现与应用&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/60649774&quot;&gt;高斯混合模型(Gaussian Mixture Model)与EM算法原理(一)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/61103099&quot;&gt;高斯混合模型(Gaussian Mixture Model)与EM算法原理(二)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://aandds.com/blog/gmm.html&quot;&gt;GMM (Gaussian Mixture Model)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/stable/modules/mixture.html&quot;&gt;【ScikitLearn】高斯混合模型&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第五节：支持向量机</title><link>https://xingguang641.com/posts/support-vector-machine/support-vector-machine/</link><guid isPermaLink="true">https://xingguang641.com/posts/support-vector-machine/support-vector-machine/</guid><description>介绍机器学习常见的模型</description><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：我们的教程终于来到了机器学习的第一个大难点 ———— 支持向量机。在深度学习盛行的今天，支持向量机是为数不多还能继续使用的传统机器学习算法之一，就让我们来看看大名鼎鼎的支持向量机到底是什么吧！（注意本篇的配图都在文字的下方）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;向量机基本原理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;以下推导部分参考自该视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=936042727&amp;amp;bvid=BV16T4y1y7qj&amp;amp;cid=494397114&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;支持向量的本质&lt;/h2&gt;
&lt;p&gt;我们首先来思考这么一个问题，如上图所示，如果要求你画一条直线，使其能够将图中的两类点分开，并且在加入新的点后也尽可能实现这个目的（具有预测能力），你会如何画这个条直线呢？直觉上来讲，这条直线靠近任何一类点都不太可行。因此我们认为，这条直线到任何一个点都足够远时，直线的分类效果最好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA2.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了实现我们上述的初步猜想，我们要先引入一个概念： &lt;strong&gt;间隔（Margin）&lt;/strong&gt;。间隔的作用是将两类数据所处的空间分隔开来，并且间隔越大，两类数据的差异也就越大。因此，要想区分两类数据，我们就得找到两类数据的最大间隔，然后我们再以间隔的正中间作为决策边界，就可以实现我们的猜想。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA3.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们将已经找到的超平面上下移动 C 个单位，使其恰好经过某些数据点，我们称这两条直线为间隔上下边界。由于间隔上下边界必然会经过几个数据点，而这几个数据点也是起到了限制间隔上下边界的作用，因此我们称这几个点为 &lt;strong&gt;支持向量（Support Vector）&lt;/strong&gt;。这便是 &lt;strong&gt;支持向量机&lt;/strong&gt;（Support Vector Machine，简称 SVM）名称的由来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA4.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;归一化技巧&lt;/h2&gt;
&lt;p&gt;对于直线方程来说，如果我们对其两边同时除以一个数，我们就可以得到一个新的方程。因此空间上的 &lt;strong&gt;一条直线&lt;/strong&gt; 拥有 &lt;strong&gt;无数&lt;/strong&gt; 个直线方程，这对我们的计算会产生影响。因此我们规定：决策上下边界的右值必须为 $\pm 1$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA5.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样我们就得到了三个平面：正超平面（Positive Hyperplane）、负超平面（Negative Hyperplane）和决策超平面（Decision Hyperplane）。&lt;/p&gt;
&lt;h2&gt;软间隔技巧&lt;/h2&gt;
&lt;p&gt;我们再进一步思考这样一个问题：如果两类数据的间隔中出现了一个异常点，那么我们计算所得的的间隔就会缩小，但我们是否要为了这个异常点而牺牲我们的间隔呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA6.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;答案是否定的。但我们要如何判断什么样的点是异常点呢？或者说，我们可以让算法自己判断是否要忽略某个数据点吗？对此，我们引入了 &lt;strong&gt;损失因子&lt;/strong&gt; 这个概念。你可以将原本的间隔视为经营的 &lt;strong&gt;收入&lt;/strong&gt; ，而将损失看作经营的 &lt;strong&gt;成本&lt;/strong&gt; ，那么我们最初的问题则可以转化为最大化 &lt;strong&gt;利润&lt;/strong&gt; 。此时的间隔我们称之为 &lt;strong&gt;软间隔（Soft Margin）&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;向量机数学建模&lt;/h2&gt;
&lt;p&gt;基本思想已经讲解清楚了，接下来我们就将我们的猜想转换为数学模型，也就是建模（由于篇幅的限制，我们将重点介绍硬间隔支持向量机，软间隔向量机将放到代码实现中的&lt;a href=&quot;#%E5%86%85%E5%AE%B9%E6%8B%93%E5%B1%95&quot;&gt;内容拓展&lt;/a&gt;中讲解）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下推导部分参考自该视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=766832077&amp;amp;bvid=BV13r4y1z7AG&amp;amp;cid=516465204&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;首先我们分别在正负超平面上任意选取一个支持向量点 $x_m$ 和 $x_n$ 。由于它们分别在正负超平面上，所以它们一定满足下列等式：&lt;/p&gt;
&lt;p&gt;$$
w_1 x_{1m} + w_2 x_{2m} + b = 1
$$&lt;/p&gt;
&lt;p&gt;$$
w_1 x_{1n} + w_2 x_{2n} + b = -1
$$&lt;/p&gt;
&lt;p&gt;我们将上述的两个等式相减后又可以得到下面这个式子：&lt;/p&gt;
&lt;p&gt;$$
w_1 (x_{1m} - x_{1n}) + w_2 (x_{2m} - x_{2n}) = 2
$$&lt;/p&gt;
&lt;p&gt;上面这个式子又相当于下面这个式子：&lt;/p&gt;
&lt;p&gt;$$
\vec{w} \cdot (\vec{x}&lt;em&gt;{m} - \vec{x}&lt;/em&gt;{n}) = 2
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA7.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再从决策超平面上随机选取两个点 $x_o$ 和 $x_p$ ，同理我们可以得到下面这个式子：&lt;/p&gt;
&lt;p&gt;$$
\vec{w} \cdot (\vec{x}&lt;em&gt;{o} - \vec{x}&lt;/em&gt;{p}) = 0
$$&lt;/p&gt;
&lt;p&gt;由此我们可以得知：$\vec{w}$ 与决策超平面垂直。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA8.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到上面的推导过程，我们将上面的点积式子用模长表示：&lt;/p&gt;
&lt;p&gt;$$
| \vec {x}_m - \vec {x}_n | * \cos \theta * | \vec {w} | = 2
$$&lt;/p&gt;
&lt;p&gt;由于 $\vec{w}$ 与决策超平面垂直，从几何含义可以得到：&lt;/p&gt;
&lt;p&gt;$$
| \vec {x}_m - \vec {x}_n | * \cos \theta = L
$$&lt;/p&gt;
&lt;p&gt;其中 $L$ 为间隔宽度。&lt;/p&gt;
&lt;p&gt;结合上面的两个式子，我们便可以得到间隔宽度 $L$ 的表达式：&lt;/p&gt;
&lt;p&gt;$$
L = \frac{2}{| \vec{w} |}
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA9.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再来看看约束条件。所有的绿点都属于正类，对应的分类值 $y_i = 1$ ，又因为它们处于正超平面的 上方，因此满足 $\vec{w} \cdot \vec{x}&lt;em&gt;{i} + b \geq 1$ ；同理，对于所有的黄点来说，它们对应的分类值 $y_i = -1$ ，满足 $\vec{w} \cdot \vec{x}&lt;/em&gt;{i} + b \leq -1$ 。&lt;/p&gt;
&lt;p&gt;综上可知，这些数据点均满足下面这个不等式：&lt;/p&gt;
&lt;p&gt;$$
y_i * (\vec{w} \cdot \vec{x}_{i} + b) \geq 1
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA10.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上面的式子我们可以知道，最大化间隔其实就是最小化 $\vec{w}$ 的模长，形式地来讲：&lt;/p&gt;
&lt;p&gt;$$
\min \left| \vec{w} \right|_2
$$&lt;/p&gt;
&lt;p&gt;$$
\text{s.t.} \quad y_i * (\vec{w} \cdot \vec{x}_{i} + b) \geq 1 \quad \forall i = 1,2,\dots,N
$$&lt;/p&gt;
&lt;p&gt;到此，我们成功地将我们的猜想转换成一个带约束的最优化问题。&lt;/p&gt;
&lt;h2&gt;向量机深层理解&lt;/h2&gt;
&lt;p&gt;看到上面的最优化问题后，许多人的第一反应应该是使用 &lt;strong&gt;拉格朗日乘数法&lt;/strong&gt; 来解决，这在一般情况下的确如此。但对于支持向量机来说，为了后续求解的效率，我们往往会将上述最优化问题转化为它的 &lt;strong&gt;拉格朗日对偶问题&lt;/strong&gt; 来求解。但我们暂且按下不表，让我们顺着拉格朗日乘数法的思路往下推导，顺便深度了解一下支持向量的含义。&lt;/p&gt;
&lt;p&gt;为了后续推导的方便，我们可以将上述的最优化问题做个小变形：&lt;/p&gt;
&lt;p&gt;$$
\min f(w) = \frac{|\vec{w}|_2^2}{2}
$$&lt;/p&gt;
&lt;p&gt;$$
\text{s.t.} \quad g_i(w, b) = y_i * (\vec{w} \cdot \vec{x}_{i} + b) - 1 \geq 0 \quad \forall i = 1,2,\dots,N
$$&lt;/p&gt;
&lt;p&gt;显然转换之后不影响最小值的求解。&lt;/p&gt;
&lt;p&gt;对于约束条件是不等式的情况，我们需要引入一个非负变量来将不等式转化为等式（我们这里之所以要将不等式转化为等式进行处理是为了从零开始推导不等式约束的拉格朗日系数必须非负这个条件，后续使用拉格朗日乘数法时不再使用，可以直接将不等式写入拉格朗日函数）：&lt;/p&gt;
&lt;p&gt;$$
\text{s.t.} \quad g_i(w, b) = y_i * (\vec{w} \cdot \vec{x}_{i} + b) - 1 = p_i^2 \quad \forall i = 1,2,\dots,N
$$&lt;/p&gt;
&lt;p&gt;由此我们可以得到下面这个拉格朗日方程式：&lt;/p&gt;
&lt;p&gt;$$
L(w, b, \lambda_i, p_i) = \frac{| \vec{w} |^2}{2} - \sum_{i=1}^{N} \lambda_i \Big[ y_i (\vec{w} \cdot \vec{x_i} + b) - 1 - p_i^2 \Big]
$$&lt;/p&gt;
&lt;p&gt;将拉格朗日函数对 $w$ 、$b$ 、$\lambda_i$ 和 $p_i$ 分别求导可得：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial \vec{w}} = \vec{w} - \sum_{i=1}^{N} \lambda_i y_i \vec{x_i} = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial b} = - \sum_{i=1}^{N} \lambda_i y_i = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial p_i} = 2 \lambda_i p_i = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial \lambda_i} = - \bigl( y_i (\vec{w} \cdot \vec{x_i} + b) - 1 - p_i^2 \bigr) = 0
$$&lt;/p&gt;
&lt;p&gt;联立下面两个等式可以得到：&lt;/p&gt;
&lt;p&gt;$$
\lambda_i(y_i (\vec{w} \cdot \vec{x}_i + b) - 1) = 0
$$&lt;/p&gt;
&lt;p&gt;根据条件 $y_i * (\vec{w} \cdot \vec{x}_i + b) - 1 \geq 0$ ，所以我们可以知道：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
y_i (\vec{w} \cdot \vec{x_i} + b) - 1 &amp;gt; 0, &amp;amp; \lambda_i = 0 \[6pt]
y_i (\vec{w} \cdot \vec{x_i} + b) - 1 = 0, &amp;amp; \lambda_i \ne 0
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;如果我们将 $\lambda_i$ 看成惩罚因子，那么上面的两种情况可以解释成：当数据点不在正负超平面上时，该数据点不对整体产生贡献；当数据点在正负超平面时，该数据点会对整体产生贡献。这便是支持向量的深层含义：只有 &lt;strong&gt;落在正负超平面上&lt;/strong&gt; 的数据点才会对拉格朗日函数 &lt;strong&gt;造成约束&lt;/strong&gt; 。这也符合我们在几何空间上的直观理解。&lt;/p&gt;
&lt;p&gt;并且我们还能得出拉格朗日的约束系数必须满足 $\lambda_i \geq 0$ ，因为当数据点不满足约束条件时必然有：&lt;/p&gt;
&lt;p&gt;$$
y_i * (\vec{w} \cdot \vec{x}_i + b) - 1 &amp;lt; 0
$$&lt;/p&gt;
&lt;p&gt;如果再加上 $\lambda_i &amp;lt; 0$ 则必然会有拉格朗日约束项小于零，这相当于变相鼓励支持向量机去违反约束条件，显然这种情况是不被允许的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA11.jpg&quot; alt=&quot;支持向量机图像&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;拉格朗日对偶问题&lt;/h1&gt;
&lt;p&gt;接下来的内容将是本篇博客的重点内容，也是支持向量机的核心难点内容。让我们一起来看一下什么是拉格朗日对偶问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下推导部分参考下面两个视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=890417318&amp;amp;bvid=BV1HP4y1Y79e&amp;amp;cid=503516018&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=766832077&amp;amp;bvid=BV13r4y1z7AG&amp;amp;cid=516465204&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;拉格朗日乘数法&lt;/h2&gt;
&lt;p&gt;要想理解什么是拉格朗日对偶问题，那就不得不先了解什么是拉格朗日乘数法。不知道读者在第一次接触拉格朗日乘数法的时候是否会感到好奇，为什么这样一顿操作之后就能求解出带约束下函数的极值呢？不如用优美的几何图像与严谨的数学语言来理解一遍吧。&lt;/p&gt;
&lt;p&gt;让我们来看一下这样一个简单的带约束优化问题：&lt;/p&gt;
&lt;p&gt;$$
\text{求 } f(x,y) \text{ 的最小值, 并且 } y \leq g(x)
$$&lt;/p&gt;
&lt;p&gt;$$
L(x,y) = f(x,y) + \lambda (y - g(x))
$$&lt;/p&gt;
&lt;p&gt;$$
\Downarrow
$$&lt;/p&gt;
&lt;p&gt;$$
\nabla L(x,y) = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\Downarrow
$$&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
\dfrac{\partial f(x,y)}{\partial x} + \lambda \dfrac{\partial (y - g(x))}{\partial x} = 0 \
\dfrac{\partial f(x,y)}{\partial y} + \lambda \dfrac{\partial (y - g(x))}{\partial y} = 0
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;如图所示（图中的数学符号不太契合我们现在探讨的问题，因此只需要关注图像即可），圆环套圆环表示类似于旋转抛物面的 $f(x,y)$ 的函数图像，直线则表示 $z = y - g(x) \leq 0$ 的边界线（可以想象是一个柱面投影在 $xOy$ 平面的图像）。&lt;/p&gt;
&lt;p&gt;观察图像易得，当圆环与直线相切时，这个切点便是带约束下函数的极值点。并且此时两个函数的梯度 &lt;strong&gt;恰好共线&lt;/strong&gt; ，如果再调节 $\lambda$ 的值则有可能使其 &lt;strong&gt;正负相互抵消&lt;/strong&gt; ，因此将拉格朗日函数写成上面的形式（将原函数与约束线性组合）后再求导便可以得到带约束条件下函数的极值点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E5%AF%B9%E5%81%B6%E9%97%AE%E9%A2%981.jpg&quot; alt=&quot;拉格朗日对偶问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再来看看在多个约束下的几何图像是什么样的。如图所示，我们再添加一个约束，此时两个约束的相交点为最优解，两个约束的梯度向量的线性组合可能会与函数的梯度向量共线且相等，对拉格朗日函数求导仍然可以得到带约束条件下函数的极值点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E5%AF%B9%E5%81%B6%E9%97%AE%E9%A2%982.jpg&quot; alt=&quot;拉格朗日对偶问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果我们再加入一个约束，如图所示，这个新加入的约束并不会改变先前所有约束条件的交集的形状，因此它的加入并不会对答案造成影响。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Csupport-vector-machine%5C%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E5%AF%B9%E5%81%B6%E9%97%AE%E9%A2%983.jpg&quot; alt=&quot;拉格朗日对偶问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这其实是 &lt;strong&gt;KKT 条件&lt;/strong&gt;（关于什么是 KKT 条件我会在下文介绍）中 &lt;strong&gt;互补松弛条件（Complementary Slackness Condition）&lt;/strong&gt; 的几何解释。如果一个条件对答案有影响，那么它对应的 $\lambda_i$ 必然大于零，我们则称其为 &lt;strong&gt;紧致条件&lt;/strong&gt;；如果一个条件对答案没有影响，那么它对应的 $\lambda_i$ 必然等于零，我们则称其为 &lt;strong&gt;松弛条件&lt;/strong&gt;（因为 $\lambda &amp;lt; 0$ 会导致约束条件的梯度向量与函数的梯度向量同向而无法相互抵消，因此不可能出现）。由此我们又可以从 &lt;strong&gt;互补松弛的角度&lt;/strong&gt; 再次了解什么是支持向量：支持向量是 &lt;strong&gt;支持向量机最优化问题中的紧致条件&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;凸问题与凸优化&lt;/h2&gt;
&lt;p&gt;拉格朗日乘数法非常强大，但它的缺点也非常明显：只能求解极值点/鞍点。拉格朗日乘数法并不能保证求解出的结果一定是最值点（但一定包含最值点），但如果我们要求解的问题是一个 &lt;strong&gt;凸问题（Convex Problem）&lt;/strong&gt;，那么这个问题中的极值点就是最值点（凸问题的性质）。&lt;/p&gt;
&lt;p&gt;而我们的拉格朗日对偶问题有个非常美妙的结论：原问题的拉格朗日对偶问题 &lt;strong&gt;一定是凸问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;接下来我们就仔细地讲解一下拉格朗日对偶问题的推导过程，首先需要将拉格朗日问题稍微改写一下：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\min \quad &amp;amp; f_0(x), \quad x \in \mathbb{R}^n \
\text{s.t.} \quad &amp;amp; f_i(x) \leq 0, \quad i = 1, 2, \dots, m \
&amp;amp; h_i(x) = 0, \quad i = 1, 2, \dots, q
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;$$
\Downarrow
$$&lt;/p&gt;
&lt;p&gt;$$
L(x, \lambda, \nu) = f_0(x) + \sum_{i=1}^m \lambda_i f_i(x) + \sum_{i=1}^q \nu_i h_i(x) \
\text{原问题：} \min_x \max_{\lambda, \nu} L(x, \lambda, \nu) \
\quad \text{s.t.} \quad \lambda \geq 0
$$&lt;/p&gt;
&lt;p&gt;当 $x$ 不在可行域内时有： $f_i(x) &amp;gt; 0$ 和 $h_i(x) ≠ 0$ 。若想最大化 $L(x, \lambda, \nu)$ ，我们可以让 $\lambda_i$ 取到正无穷，让 $\nu_i h_i(x)$ 取到正无穷：&lt;/p&gt;
&lt;p&gt;$$
\max_{\lambda, \nu} L(x, \lambda, \nu) = f_0(x) + \infty + \infty = \infty
$$&lt;/p&gt;
&lt;p&gt;当 $x$ 在可行域内时有： $f_i(x) \leq 0$ 和 $h_i(x) = 0$ 。若想最大化 $L(x, \lambda, \nu)$ ，我们可以让 $\lambda_i$ 取到 0（因为 $h_i(x) = 0$ ，所以 $\nu_i$ 取任意值均可）：&lt;/p&gt;
&lt;p&gt;$$
\max_{\lambda, \nu} L(x, \lambda, \nu) = f_0(x) + 0 + 0 = f_0(x)
$$&lt;/p&gt;
&lt;p&gt;然后在此基础上再取 $min$ 可以得到：&lt;/p&gt;
&lt;p&gt;$$
\min_{x} \max_{\lambda, \nu} L(x, \lambda, \nu) = \min_{x} { f_0(x), \infty } = \min_{x} f_0(x)
$$&lt;/p&gt;
&lt;p&gt;因此拉格朗日问题的两种形式是等价的。&lt;/p&gt;
&lt;p&gt;接下来我们看看拉格朗日对偶问题的数学形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\text{对偶函数：} &amp;amp; \quad g(\lambda, \nu) = \min_{x} L(x, \lambda, \nu) \
\text{对偶问题：} &amp;amp; \quad \max_{\lambda, \nu} g(\lambda, \nu) = \max_{\lambda, \nu} \min_{x} L(x, \lambda, \nu) \
&amp;amp; \quad \text{s.t. } \lambda \geq 0
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;很明显，原问题就是先求 $max$ 再求 $min$ 的过程，对偶问题就是先求 $min$ 再求 $max$ 的过程。&lt;/p&gt;
&lt;p&gt;我们来看看凸问题的定义：当一个问题的 &lt;strong&gt;约束条件是凸集&lt;/strong&gt; 且 &lt;strong&gt;问题函数为凸函数&lt;/strong&gt; 时，该问题被称为凸问题。观察对偶函数 $L(x, \lambda, \nu)$ ，如果先对参数 $x$ 做最小值优化，则在做最大值优化的时候， $f_0(x^&lt;em&gt;)$ 、 $f_i(x^&lt;/em&gt;)$ 和 $h_i(x^*)$ 都是常数，也就是说：此时的对偶函数 $g(\lambda, \nu)$ 是一个 &lt;strong&gt;线性函数&lt;/strong&gt; ，而线性函数是一个 &lt;strong&gt;凸函数&lt;/strong&gt; 。再加上对偶问题的约束条件是 $\lambda \geq 0$ ，这是一个 &lt;strong&gt;半空间&lt;/strong&gt; ，而半空间是一个 &lt;strong&gt;凸集&lt;/strong&gt; 。综上所述，对偶问题是一个 &lt;strong&gt;凸问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;$$
g(\lambda, \nu) = f_0(x^&lt;em&gt;) + \sum_{i=1}^m \lambda_i f_i(x^&lt;/em&gt;) + \sum_{i=1}^q \nu_i h_i(x^*)
$$&lt;/p&gt;
&lt;h2&gt;弱对偶与强对偶&lt;/h2&gt;
&lt;p&gt;到此为止，我们已经知道了什么是拉格朗日对偶问题，也弄懂为什么拉格朗日对偶问题一定是一个凸问题。但我们要想用拉格朗日对偶问题来解决原问题，就必须证明两个问题得到的解之间是相关的，否则对偶问题有再多优美的性质，也无法帮我们去解决原问题。所以接下来，就让我们来看一下拉格朗日问题与其对偶问题之间的关系吧。&lt;/p&gt;
&lt;p&gt;首先我们可以轻松地证明出：拉格朗日问题的解大于等于其对偶问题的解。下面是证明过程：&lt;/p&gt;
&lt;p&gt;$$
\max_{\lambda, \nu} L(x, \lambda, \nu) \ge L(x, \lambda, \nu) \ge \min_{x} L(x, \lambda, \nu)
$$&lt;/p&gt;
&lt;p&gt;$$
A(x) = \max_{\lambda, \nu} L(x, \lambda, \nu) \ge L(x, \lambda, \nu) \ge \min_{x} L(x, \lambda, \nu) = I(\lambda, \nu)
$$&lt;/p&gt;
&lt;p&gt;$$
A(x) \ge I(\lambda, \nu) \quad (\forall, x, \lambda, \nu)
$$&lt;/p&gt;
&lt;p&gt;$$
A(x) \ge \min_{x} A(x) \ge \max_{\lambda, \nu} I(\lambda, \nu) \ge I(\lambda, \nu)
$$&lt;/p&gt;
&lt;p&gt;$$
P^{&lt;em&gt;} = \min_{x} A(x) \ge \max_{\lambda, \nu} I(\lambda, \nu) = D^{&lt;/em&gt;}
$$&lt;/p&gt;
&lt;p&gt;而上述这个性质我们称之为 &lt;strong&gt;弱对偶性（Weak Duality Theorem）&lt;/strong&gt;。也就是说：拉格朗日对偶问题与原问题 &lt;strong&gt;一定满足弱对偶性&lt;/strong&gt; 。那么拉格朗日对偶问题在什么条件下与原问题之间满足 &lt;strong&gt;强对偶性（Strong Duality Theorem）&lt;/strong&gt; 呢？当原问题是 &lt;strong&gt;凸问题&lt;/strong&gt; 且满足一定的 &lt;strong&gt;正则条件（Slater 条件就是其中之一）&lt;/strong&gt; 时，原问题与其对偶问题满足强对偶性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体证明省略，需要的读者可以观看下面这个 PDF&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://bolei-zhang.github.io/course/06be4d689c17514cfe26a9a14ddff12d3911cd62/slides/lec5_duality_1-2.pdf&quot;&gt;对偶理论（Duality）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但另一个问题也随之而来：既然原问题已经是凸问题，为什么仍要引出对偶问题这个概念呢？原因就是原问题虽然也是凸问题，但原问题本身往往非常复杂，求解起来十分困难，而对偶问题将繁杂的约束条件融入到拉格朗日函数中，求解起来十分简单，因此我们会更倾向于将原问题转化成它的对偶问题来进行求解（即便原问题不是凸问题我们也会尝试将其转化成对偶问题来求解）。&lt;/p&gt;
&lt;h2&gt;KKT 强队偶条件&lt;/h2&gt;
&lt;p&gt;原理部分讲到这里其实已经足够了，但在&lt;a href=&quot;#%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E4%B9%98%E6%95%B0%E6%B3%95&quot;&gt;先前讲解拉格朗日乘数法&lt;/a&gt;时所提到的 KKT 条件并不完整，因此在这里给出详细的 KKT 条件：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
\nabla f(x^&lt;em&gt;) + \sum_{i=1}^m \lambda_i \nabla g_i(x^&lt;/em&gt;) + \sum_{j=1}^p \nu_j \nabla h_j(x^&lt;em&gt;) = 0 &amp;amp; \text{(Stationarity)} \[1.2em]
g_i(x^&lt;/em&gt;) \le 0, \quad i = 1, \dots, m &amp;amp; \text{(Primal feasibility)} \[0.8em]
h_j(x^&lt;em&gt;) = 0, \quad j = 1, \dots, p &amp;amp; \text{(Equality constraints)} \[0.8em]
\lambda_i \ge 0, \quad i = 1, \dots m, &amp;amp; \text{(Dual feasibility)} \[0.8em]
\lambda_i g_i(x^&lt;/em&gt;) = 0, \quad i = 1, \dots, m &amp;amp; \text{(Complementary slackness)}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;KKT 条件的作用就是能让我们快速判断一个问题是否是强对偶问题：在绝大多数条件下（少数情况几乎都是人为构造，实际应用相当罕见），只要满足 KKT 条件的问题就是强队偶问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;向量机代码实现&lt;/h1&gt;
&lt;p&gt;根据拉格朗日对偶问题的原理，我们来推导一下支持向量机问题的对偶问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

# 假设 X: (n_samples, n_features), y: (n_samples,) 且值为 +1/-1
def linear_svm_train(X, y, lr=0.001, epochs=1000):
    n_samples, n_features = X.shape
    alpha = np.zeros(n_samples)

    # 梯度上升求解对偶问题
    for _ in range(epochs):
        for i in range(n_samples):
            # 对 α_i 的梯度
            grad = 1 - np.sum(alpha * y * y[i] * np.dot(X, X[i]))
            alpha[i] += lr * grad
            alpha[i] = max(alpha[i], 0)  # 保证 α_i &amp;gt;= 0

    # 计算 w
    w = np.sum((alpha * y)[:, None] * X, axis=0)

    # 找一个支持向量求 b
    sv_idx = np.where(alpha &amp;gt; 1e-5)[0][0]
    b = y[sv_idx] - np.dot(w, X[sv_idx])

    return w, b

def linear_svm_predict(X, w, b):
    # 加 sign 是为了做判别分析
    return np.sign(np.dot(X, w) + b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入拉格朗日乘子 $\alpha_i$ 并写出拉格朗日函数：&lt;/p&gt;
&lt;p&gt;$$
L(w, b, \alpha) = \frac{| \vec{w} |^2}{2} - \sum_{i=1}^{N} \alpha_i \Big[ y_i (\vec{w} \cdot \vec{x_i} + b) - 1 \Big]
$$&lt;/p&gt;
&lt;p&gt;将其对 $\vec{w}$ 和 $b$ 求偏导：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial \vec{w}} = \vec{w} - \sum_{i=1}^{s} \alpha_i y_i \vec{x_i} \quad \frac{\partial L}{\partial b} = - \sum_{i=1}^{s} \alpha_i y_i
$$&lt;/p&gt;
&lt;p&gt;令偏导为零并带入拉格朗日函数可得到支持向量机问题的对偶问题：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\max_{\alpha} \quad &amp;amp; \sum_{i=1}^{n} \alpha_i - \frac{1}{2} \sum_{i=1}^{n} \sum_{j=1}^{n} \alpha_i \alpha_j y_i y_j \vec{x_i} \cdot \vec{x_j} \
\text{s.t.} \quad &amp;amp; \alpha_i \ge 0, \quad \sum_{i=1}^{n} \alpha_i y_i = 0
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;接下来我们只需要用梯度上升（这里是求 $max$）的方式求解出参数 $\alpha$ 即可。&lt;/p&gt;
&lt;h2&gt;内容拓展&lt;/h2&gt;
&lt;p&gt;支持向量机其实还有很多可以优化的点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先就是上面的梯度上升算法：对于支持向量机来说，直接使用梯度上升算法是不太行的，由于对偶问题有 &lt;strong&gt;等式约束&lt;/strong&gt; ，直接梯度很难保持约束。不仅如此，计算梯度还会遇到开销大、收敛慢等问题。因此我们往往会使用 &lt;strong&gt;SMO 算法&lt;/strong&gt; 来替代梯度上升算法（SMO 算法的具体内容可以看下面的博客）。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/560529392&quot;&gt;详细推导序列最小优化SMO算法&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后就是对于线性不可分的数据：我们在前面这么长的推导过程其实都有一个前提假设 ———— 数据是线性可分的。但是在大多数情况下，数据往往是线性不可分的，那我们就得要引出我们的 &lt;strong&gt;核技巧（Kernel Trick）&lt;/strong&gt; 了。核技巧的原理非常简单，就是想办法将数据升维后，再进行支持向量机的构建，因为 &lt;strong&gt;维度越高数据越有可能线性可分&lt;/strong&gt; （具体讲解可以看下面这个视频，限于篇幅原因不过多讲解）。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=637192057&amp;amp;bvid=BV1Nb4y1s7pE&amp;amp;cid=547472956&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后就是软间隔问题：在&lt;a href=&quot;#%E8%BD%AF%E9%97%B4%E9%9A%94&quot;&gt;上文&lt;/a&gt;我们讲到过什么是软间隔，但我们没有过多解释软间隔的数学原理，如果读者感兴趣可以观看下面的视频。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=682759064&amp;amp;bvid=BV1AS4y1K7Jf&amp;amp;cid=565289579&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么说支持向量机是一个自带 L2 正则化的机器学习算法？（什么是合叶损失函数？）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下推导部分参考自该视频&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=549434532&amp;amp;bvid=BV1zq4y1g74J&amp;amp;cid=449456397&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;首先给出软间隔下的支持向量机最优化问题：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\min_{W,b,\xi} \quad &amp;amp; \frac{1}{2} W^T W + C \sum_{i=1}^N \xi_i \
\text{s.t.} \quad &amp;amp; 1 - Y^{(i)} (W^T X^{(i)} + b) \leq \xi_i, \quad i = 1, 2, \dots, N \
&amp;amp; \xi_i \geq 0, \quad i = 1, 2, \dots, N
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;然后转换成如下形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
\min_{W,b,\xi} \quad &amp;amp; \frac{1}{2} W^T W + C \cdot \sum_{i=1}^N \xi_i \
\text{s.t.} \quad &amp;amp;
\begin{aligned}
\xi_i &amp;amp;\geq \max\left{0, 1 - Y^{(i)}(W^T \cdot X^{(i)} + b)\right}, &amp;amp; i = 1, 2, \dots, N \
&amp;amp;= \left[1 - Y^{(i)}(W^T \cdot X^{(i)} + b)\right]_+, &amp;amp; i = 1, 2, \dots, N
\end{aligned}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;最后将问题转化为拉格朗日函数可得：&lt;/p&gt;
&lt;p&gt;$$
\min \frac{1}{2} W^T W + C \sum_{i=1}^{N} \left[ 1 - Y^{(i)} (W^T X^{(i)} + b) \right]_+
$$&lt;/p&gt;
&lt;p&gt;$$
\Downarrow
$$&lt;/p&gt;
&lt;p&gt;$$
\min \underbrace{\sum_{i=1}^{N} \left[ 1 - Y^{(i)} (W^T X^{(i)} + b) \right]&lt;em&gt;+}&lt;/em&gt;{\textcolor{green}{\text{经验损失项}}}
+
\underbrace{\lambda \frac{1}{2} W^T W}_{\textcolor{green}{\text{正则化项}}}
$$&lt;/p&gt;
&lt;p&gt;上述形式中 “经验损失项” 就是所谓的 “合叶损失函数” ，“正则化项” 就是 “L2 正则化” 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/fyc1314/article/details/153789016&quot;&gt;支持向量机（SVM）详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/stable/modules/svm.html&quot;&gt;【ScikitLearn】支持向量机&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/v20000727/article/details/135137095&quot;&gt;什么是支持向量机&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.zhihu.com/tardis/zm/art/31886934&quot;&gt;支持向量机（SVM）——原理篇&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA&quot;&gt;【维基百科】支持向量机&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/49331510&quot;&gt;看了这篇文章你还不懂SVM你就来打我&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ww2.mathworks.cn/discovery/support-vector-machine.html&quot;&gt;【MathWorks】支持向量机的工作原理&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://note.wcoder.com/MachineLearning/%E5%9B%BE%E8%A7%A3%E6%95%B0%E5%AD%A6/files/%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA.pdf&quot;&gt;【图解数学】支持向量机&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第四节：朴素贝叶斯分类器</title><link>https://xingguang641.com/posts/naive-bayes-classifier/naive-bayes-classifier/</link><guid isPermaLink="true">https://xingguang641.com/posts/naive-bayes-classifier/naive-bayes-classifier/</guid><description>介绍机器学习常见的模型</description><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;朴素贝叶斯分类器基本原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;朴素贝叶斯分类器（Naive Bayes Classifier）&lt;/strong&gt; 是一种基于 &lt;strong&gt;贝叶斯定理（Bayes&apos; Theorem）&lt;/strong&gt; 并假设各个特征之间 &lt;strong&gt;相互独立&lt;/strong&gt; 的概率分类模型。它属于 &lt;strong&gt;生成式模型&lt;/strong&gt; ，常用于文本分类、垃圾邮件检测、情感分析、医学诊断等任务。&lt;/p&gt;
&lt;p&gt;朴素贝叶斯分类器是 &lt;strong&gt;贝叶斯决策论&lt;/strong&gt; 在分类任务中的一个具体实现，它通过条件独立假设来 &lt;strong&gt;近似实现&lt;/strong&gt; 贝叶斯最优决策规则。因此我们要想了解朴素贝叶斯分类器，首先就得知道什么是贝叶斯决策论。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cnaive-bayes-classifier%5C%E8%B4%9D%E5%8F%B6%E6%96%AF%E8%82%96%E5%83%8F.gif&quot; alt=&quot;贝叶斯肖像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;最优分类器&lt;/h2&gt;
&lt;p&gt;假设有 $N$ 种可能的类别标记，即 $Y = { c_1, c_2, \dots, c_N }$ 是将一个真实标记为 ​$c_i$ 的样本误分类为 $c_j$ 所产生的损失，基于后验概率 $P(c_i|x)$ 可获得将样本 $x$ 分类为 $c_i$ 所产生的期望损失（Expected Loss）, 即在样本 $P(c_i | x)$ 上的 &lt;strong&gt;条件风险（Conditional Risk）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$$
R(c_i|x) = \sum_{j=1}^N \lambda_{ij} P(c_j|x)
$$&lt;/p&gt;
&lt;p&gt;我们的任务是寻找一个判定准则 $h : X \mapsto Y$ ，以最小化总体风险：&lt;/p&gt;
&lt;p&gt;$$
R(h) = \mathbb{E}_{x} \Big[ R(h(x)|x) \Big]
$$&lt;/p&gt;
&lt;p&gt;显然，对每个样本 $x$ 若 $h$ 能最小化条件风险 $R(h(x) | x)$ ，则总体风险 $R(h)$ 也将被最小化，这就产生了贝叶斯判定准则（Bayes Decision Rule）：为最小化总体风险，只需在每个样本上选择那个能使条件风险 $R(c | x)$ 最小的类别标记（要想均值最小化，只需要最小化每一个样本）。&lt;/p&gt;
&lt;p&gt;$$
h^*(x) = \arg \min_{c \in \mathcal{Y}} R(c|x)
$$&lt;/p&gt;
&lt;p&gt;此时 $h^&lt;em&gt;$ 称为 &lt;strong&gt;贝叶斯最优分类器（Bayes Optimal Classifier）&lt;/strong&gt;，与之对应的总体风险 $R(h^&lt;/em&gt;)$ 称为 &lt;strong&gt;贝叶斯风险（Bayes Risk）&lt;/strong&gt;。而 $1 - R(h^*)$ 则反映了分类器所能达到的最佳性能，即通过机器学习所能产生的模型精度的理论上限。&lt;/p&gt;
&lt;p&gt;具体来说，若目标是最小化分类错误率，则误判损失函数可写为：&lt;/p&gt;
&lt;p&gt;$$
\lambda_{ij} =
\begin{cases}
0, &amp;amp; i = j \
1, &amp;amp; i \neq j
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;此时条件风险为 $R(c|x) = 1 - P(c|x)$ ，于是最小化分类错误率的贝叶斯最优分类器可以写作：&lt;/p&gt;
&lt;p&gt;$$
h^*(x) = \arg \max_{c \in \mathcal{Y}} P(c|x)
$$&lt;/p&gt;
&lt;p&gt;即对每个样本应选择能使后验概率 $P(c|x)$ 最大的类别标记。&lt;/p&gt;
&lt;h2&gt;朴素贝叶斯&lt;/h2&gt;
&lt;p&gt;不难看出，欲使用贝叶斯判定准则来最小化决策风险，首先要获得后验概率 $P(c|x)$ 。然而，在现实任务中这通常难以直接获得。从这个角度来看，机器学习所要实现的是 &lt;strong&gt;基于有限的训练样本集&lt;/strong&gt; 尽可能准确地 &lt;strong&gt;估计&lt;/strong&gt; 出后验概率 $P(c|x)$ 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cnaive-bayes-classifier%5C%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB%E5%99%A81.jpg&quot; alt=&quot;朴素贝叶斯分类器图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大体来说，主要有两种策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过直接建模 $P(c|x)$ 来预测 $c$ ，这样得到的是 &lt;strong&gt;判别式模型（Discriminative Models）&lt;/strong&gt;，代表是决策树、BP 神经网络、支持向量机等&lt;/li&gt;
&lt;li&gt;先对联合概率分布 $P(x, c)$ 建模，然后再由此获得 $P(c|x)$ ，这样得到的是 &lt;strong&gt;生成式模型（Generative Models）&lt;/strong&gt;，代表是贝叶斯分类器等&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;判别式模型是给定特征的情况下预测这个样本的标签&lt;/p&gt;
&lt;p&gt;生成式模型是假设样本的分布特征后构建分类模型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于生成式模型，必然考虑：&lt;/p&gt;
&lt;p&gt;$$
P(c|x) = \frac{P(x, c)}{P(x)} = \frac{P(c)P(x|c)}{P(x)}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$P(c)$ 是类 &lt;strong&gt;先验&lt;/strong&gt; （Prior）概率，表达了样本空间中各类样本所占的比例，可通过各类样本出现的频率来进行估计（大数定律）&lt;/li&gt;
&lt;li&gt;$P(c|x)$ 是样本 $x$ 相对于类标记 $c$ 的类 &lt;strong&gt;条件概率（Class-Conditional Probability）&lt;/strong&gt;，或称为 &lt;strong&gt;似然（Likelihood）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;$P(x)$ 是用于归一化的 &lt;strong&gt;证据（Evidence）&lt;/strong&gt; 因子，与类别无关，可以不考虑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不难发现，基于贝叶斯公式来估计后验概率 $P(c|x)$ 的主要困难在于：类条件概率 $P(x|c)$ 是所有属性上的联合概率，难以从有限的训练样本直接估计而得。为避开这个障碍，朴素贝叶斯分类器采用了 &lt;strong&gt;属性条件独立性假设（Attribute Conditional Independence Assumption）&lt;/strong&gt;：对已知类别，假设所有属性相互独立。&lt;/p&gt;
&lt;p&gt;基于属性条件独立性假设，贝叶斯公式可重写为：&lt;/p&gt;
&lt;p&gt;$$
P(c|x) = \frac{P(c)P(x|c)}{P(x)} = \frac{P(c)}{P(x)} \prod_{i=1}^d P(x_i|c)
$$&lt;/p&gt;
&lt;p&gt;由于对所有类别来说 $P(x)$ 相同，因此基于贝叶斯判定准则有：&lt;/p&gt;
&lt;p&gt;$$
h_{nb}(x) = \arg\max_{c \in \mathcal{Y}} P(c) \prod_{i=1}^{d} P(x_i|c)
$$&lt;/p&gt;
&lt;p&gt;这就是朴素贝叶斯分类器的表达式。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;朴素贝叶斯分类器代码实现&lt;/h1&gt;
&lt;p&gt;在给出代码之前，我们先介绍一个重要的操作 ———— &lt;strong&gt;拉普拉斯平滑（Laplace Smoothing）&lt;/strong&gt;。在朴素贝叶斯模型中，如果某个特征 &lt;strong&gt;在训练数据中从未出现过&lt;/strong&gt; ，那么模型会将其对应的概率估计为 0 。这样一来，当我们计算类别的联合概率（多个特征概率的乘积）时，只要出现一个 0 ，整个结果就会变成 0 ，导致分类器彻底失效。为了解决这个问题，我们需要对类条件概率 $P(c_i|x)$ 进行适当修正，即在统计频数的基础上加入一个平滑项，从而避免出现零概率的情况。&lt;/p&gt;
&lt;p&gt;除了 “朴素” 假设外，朴素贝叶斯分类器还要假定样本的分布，因此根据假设分布的不同，朴素贝叶斯分类器还分为不同的种类。下面我们就介绍最常见的三种朴素贝叶斯分类器，并且因为朴素贝叶斯分类器实现繁杂（原理很简单），我们就用 ScikitLearn 机器学习库来实现。&lt;/p&gt;
&lt;p&gt;对于任何一个朴素贝叶斯分类器来说，我们只需要求解出 $P(c)$ 与 $P(x|c)$ 即可（并且朴素贝叶斯分类器也是一个 “闭式解” 模型，因此无需梯度迭代）。$P(c)$ 的求解方式是用频率估计概率，也就是直接用样本计数的方式求解；$P(x|c)$ 则是我们的假设分布，对于不同的朴素贝叶斯分类器，我们有不同的假设分布。&lt;/p&gt;
&lt;h2&gt;1. 高斯贝叶斯&lt;/h2&gt;
&lt;p&gt;对于高斯贝叶斯分类器，假设分布如下：&lt;/p&gt;
&lt;p&gt;$$
P(x_i | c) = \frac{1}{\sqrt{2\pi\sigma_{c,i}^2}}
\exp\left(-\frac{(x_i - \mu_{c,i})^2}{2\sigma_{c,i}^2}\right)
$$&lt;/p&gt;
&lt;p&gt;由于求解高斯分布只需要用到样本的均值和方差，因此无需做拉普拉斯平滑操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.naive_bayes import GaussianNB
X, y = make_classification(
    n_samples=500, n_features=2, n_redundant=0, n_informative=2,
    n_clusters_per_class=1, random_state=42
)


# 执行代码
if __name__ == &quot;__main__&quot;:
    # 模型训练
    model = GaussianNB()
    model.fit(X, y)

    # 绘制决策边界
    xx, yy = np.meshgrid(
        np.linspace(X[:,0].min()-1, X[:,0].max()+1, 200),
        np.linspace(X[:,1].min()-1, X[:,1].max()+1, 200)
    )
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1].reshape(xx.shape)

    plt.figure(figsize=(7,6))
    plt.contourf(xx, yy, Z, cmap=&apos;RdBu&apos;, alpha=0.6)
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=&apos;RdBu&apos;, edgecolor=&apos;k&apos;)
    plt.title(&quot;Decision Boundary of Gaussian Naive Bayes&quot;, fontsize=14)
    plt.xlabel(&quot;Feature 1&quot;)
    plt.ylabel(&quot;Feature 2&quot;)
    plt.colorbar(label=&quot;P(y=1|x)&quot;)
    plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 多项式贝叶斯&lt;/h2&gt;
&lt;p&gt;对于多项式贝叶斯分类器，假设分布如下：&lt;/p&gt;
&lt;p&gt;$$
P(x_i | c) = \frac{(\sum_i x_i)!}{\prod_i x_i!}
\prod_i \theta_{c,i}^{x_i}
\quad \text{where }
\theta_{c,i} = \frac{N_{c,i} + \alpha}{\sum_j (N_{c,j} + \alpha)}
$$&lt;/p&gt;
&lt;p&gt;上述公式中已使用拉普拉斯平滑操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
# 文本数据
texts = [&quot;I love machine learning&quot;, 
        &quot;Naive Bayes is simple&quot;,
        &quot;I love learning&quot;,
        &quot;Bayes theorem is powerful&quot;
]
y = [1, 0, 1, 0]
# 转换为词频向量
vec = CountVectorizer()
X = vec.fit_transform(texts)

# 执行代码
if __name__ == &quot;__main__&quot;:
    # 模型训练
    model = MultinomialNB(alpha=1.0)  # alpha=1 表示拉普拉斯修正
    model.fit(X, y)

    # 预测
    test = [&quot;I love Bayes&quot;]
    X_test = vec.transform(test)
    print(&quot;Predicted label:&quot;, model.predict(X_test))
    print(&quot;Posterior probabilities:&quot;, model.predict_proba(X_test))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 伯努利贝叶斯&lt;/h2&gt;
&lt;p&gt;对于伯努利贝叶斯分类器，假设分布如下：&lt;/p&gt;
&lt;p&gt;$$
P(x_i | c) = \prod_i p_{c,i}^{x_i} (1 - p_{c,i})^{1 - x_i}
\quad \text{where }
p_{c,i} = \frac{N_{c,i} + \alpha}{N_y + 2\alpha}
$$&lt;/p&gt;
&lt;p&gt;无论是伯努利贝叶斯还是多项式贝叶斯，其假设分布都需要用古典概型的方式进行拟合，有可能会产生 0 从而导致分类器失效，因此需要对它们使用拉普拉斯平滑操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer
texts = [&quot;I love AI&quot;, &quot;AI is fun&quot;, &quot;I hate bugs&quot;, &quot;bugs are annoying&quot;]
y = [1, 1, 0, 0]
# 转换为 0/1 特征矩阵
vec = CountVectorizer(binary=True)
X = vec.fit_transform(texts)

# 执行代码
if __name__ == &quot;__main__&quot;:
    # 模型训练
    model = BernoulliNB(alpha=1.0)
    model.fit(X, y)

    # 预测
    test = [&quot;I love bugs&quot;]
    X_test = vec.transform(test)
    print(&quot;Predicted label:&quot;, model.predict(X_test))
    print(&quot;Posterior probabilities:&quot;, model.predict_proba(X_test))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 内容拓展&lt;/h2&gt;
&lt;p&gt;尽管它们的假设看起来过于简化，但朴素贝叶斯分类器在许多实际情况下表现良好，著名的例子包括文档分类和垃圾邮件过滤。它们只需要少量训练数据就能估计必要的参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于为什么朴素贝叶斯效果好以及在哪些类型的数据上效果好的理论原因，请参见下面的参考文献&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cs.unb.ca/~hzhang/publications/FLAIRS04ZhangH.pdf&quot;&gt;The optimality of Naive Bayes.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;与更复杂的方法相比，朴素贝叶斯分类器的速度非常快。类条件特征分布的解耦意味着每个分布都可以独立地作为一维分布进行估计。这反过来有助于减轻由维数灾难引起的问题。&lt;/p&gt;
&lt;p&gt;另一方面，虽然朴素贝叶斯被认为是一个不错的分类器，但它也是一个糟糕的估计器，因此 &lt;code&gt;predict_proba&lt;/code&gt; 的概率输出不应过于重视。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;什么是半朴素贝叶斯分类器？其与朴素贝叶斯分类器的区别是什么？（朴素贝叶斯分类器的局限是什么？）&lt;/p&gt;
&lt;p&gt;在现实生活中，属性条件独立性假设往往很难成立，于是人们尝试对属性条件独立性假设进行一定程度的放松，由此产生了一类称为 &lt;strong&gt;半朴素贝叶斯分类器（Semi-Naive Bayes Classifiers）&lt;/strong&gt; 的学习方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cnaive-bayes-classifier%5C%E5%8D%8A%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB%E5%99%A81.png&quot; alt=&quot;半朴素贝叶斯分类器图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;独依赖估计&lt;/strong&gt;（One-Dependent Estimator, 简称 ODE）是半朴素贝叶斯分类器最常用的一种策略。顾名思议，所谓 “独依赖” 就是假设每个属性在类别之外最多仅依赖于一个其他属性，即：&lt;/p&gt;
&lt;p&gt;$$
P(c | x) \propto P(c) \prod_{i=1}^d P(x_i | c, pa(i))
$$&lt;/p&gt;
&lt;p&gt;其中 $pa(i)$ 为属性 $x_i$ 所依赖的属性，称为 $x_i$ 的父属性。此时，对每个属性 $x_i$ 若其父属性 $pa(i)$ 已知，则可采用频率估计概率的办法来估计概率值 $P(x_i | c, pa(i))$ 。于是，问题的关键就转化为如何确定每个属性的父属性，不同的做法产生不同的独依赖分类器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPODE（Super-Parent ODE）方法假设所有属性都依赖于同一个属性，然后通过交叉验证等模型选择方法来确定超父属性&lt;/li&gt;
&lt;li&gt;TAN（Tree Augmented naive Bayes）则是通过计算任意两个属性之间的条件互信息，构建最大带权生成树，从而将属性间依赖关系约简为树形结构，仅保留强相关属性之间的依赖性，条件互信息的公式为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
I(x_i, x_j | y) =
\sum_{x_i, x_j; c \in \mathcal{Y}}
P(x_i, x_j | c)
\log \frac{P(x_i, x_j | c)}{P(x_i | c) P(x_j | c)}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AOED（Averaged One-Dependent Estimator）是一种集成学习机制的独依赖分类器，尝试将每个属性作为超父来构建 SPODE，然后将那些具有足够训练数据支撑的 SPODE 集成起来作为最终结果，即：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
P(c | x) \propto
\sum_{\substack{i=1 \ |D_{x_i}| \ge m&apos;}}^{d}
P(c, x_i)
\prod_{j=1}^{d} P(x_j | c, x_i)
$$&lt;/p&gt;
&lt;p&gt;不难看出，与朴素贝叶斯分类器类似，AOED 无需模型选择，既能通过预计算节省预测时间，也能采取懒惰学习方式在预测时再进行计数，并且易于实现增量学习。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多半朴素贝叶斯分类器的内容可以看下面两个博客&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://baidinghub.github.io/2020/04/03/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%85%AD%EF%BC%89%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB%E5%99%A8/&quot;&gt;机器学习（六）贝叶斯分类器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_34222839/article/details/147490529&quot;&gt;【机器学习基础】第二十四课：半朴素贝叶斯分类器&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何直观理解对条件概率建模表示判别模型，对联合概率建模表示生成模型？（究竟什么是判别模型？什么是生成模型？为什么朴素贝叶斯对后验概率建模却是生成模型？）&lt;/p&gt;
&lt;p&gt;若想解决上述问题，我们只需要弄清楚两种模型的目的分别是什么即可。&lt;/p&gt;
&lt;p&gt;判别模型的思路是：我不关心 $x$ 是怎么来的，我只关心不同类别之间的分界。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因此给定一个 $x$ ，我们只需要判断它属于哪个 $y$ 即可，因此我们可以直接对条件概率 $P(y|x)$ 建模。判别模型要做的就是 &lt;strong&gt;在某个空间中&lt;/strong&gt; ，用 &lt;strong&gt;一个超平面&lt;/strong&gt; 将数据进行区分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生成模型的思路是：我先去理解每个类别的数据究竟是如何产生的（由此可以构建生成器），再根据贝叶斯公式去构建判别器（更准确地说是在做预测）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因此生成模型本质是在建模 $P(x|y)$ 。但根据公式 $P(x, y) = P(x|y)P(y)$ 我们可以知道，建模后验概率就是在建模联合概率。因此我们 &lt;strong&gt;通常说建模联合概率表示生成模型，但本质是在建模后验概率&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/FFMXjy/article/details/145255053&quot;&gt;朴素贝叶斯分类器(Naive Bayes Classifier)教程&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/m0_52049033/article/details/143114512&quot;&gt;Naive Bayes（朴素贝叶斯分类器）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Naive_Bayes_classifier#Probabilistic_model&quot;&gt;【维基百科】朴素贝叶斯分类器&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/wjjc1017/article/details/141768420&quot;&gt;伯努利朴素贝叶斯详解：初学者的可视化指南与代码示例&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/MarisaMagic/p/17948124&quot;&gt;[NLP复习笔记] 朴素贝叶斯分类器&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/1.6/modules/naive_bayes.html&quot;&gt;【ScikitLearn】朴素贝叶斯&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zhroyn.github.io/MyNotes/%E8%AF%BE%E7%A8%8B/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB%E5%99%A8.html&quot;&gt;Zhroyn 的学习笔记之贝叶斯分类器&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第三节：高斯判别分析</title><link>https://xingguang641.com/posts/regression-model/gaussian-discriminant-analysis/</link><guid isPermaLink="true">https://xingguang641.com/posts/regression-model/gaussian-discriminant-analysis/</guid><description>介绍机器学习常见的模型</description><pubDate>Fri, 24 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;高斯判别基本原理&lt;/h1&gt;
&lt;p&gt;虽然逻辑回归在机器学习任务中的效果非常好，但在样本呈现特殊分布的情况下，我们可以使用其他更好的算法。&lt;strong&gt;高斯判别分析&lt;/strong&gt;（Gaussian Discriminant Analysis，简称 GDA）就是其中的一个。这篇博客的主要内容，就是介绍高斯判别分析算法的主要原理以及公式的推导。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cregression-model%5C%E9%AB%98%E6%96%AF%E5%88%A4%E5%88%AB%E6%A8%A1%E5%9E%8B1.jpg&quot; alt=&quot;高斯判别分析图像&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;基本先验假设&lt;/h2&gt;
&lt;p&gt;与逻辑回归不同，高斯判别分析需要两个先验假设，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类别标签 $y$ 服从伯努利分布&lt;/p&gt;
&lt;p&gt;$$
P(y) =
\begin{cases}
\phi^{y}(1 - \phi)^{1 - y} &amp;amp; y = 0, 1 \\
0 &amp;amp; y \ne 0, 1
\end{cases}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;正负样本均符合正态分布&lt;/p&gt;
&lt;p&gt;$$
P(x \mid y = 0)
= \frac{1}{(2\pi)^{\frac{n}{2}} |\Sigma|^{\frac{1}{2}}}
\exp!\left(
-\frac{1}{2} (x - \mu_0)^{\rm T} \Sigma^{-1} (x - \mu_0)
\right)
$$&lt;/p&gt;
&lt;p&gt;$$
P(x \mid y = 1)
= \frac{1}{(2\pi)^{\frac{n}{2}} |\Sigma|^{\frac{1}{2}}}
\exp!\left(
-\frac{1}{2} (x - \mu_1)^{\rm T} \Sigma^{-1} (x - \mu_1)
\right)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正因为在模型中我们需要预先假设样本服从正态分布，这也是 “高斯判别分析” 名字的由来。&lt;/p&gt;
&lt;p&gt;有了以上的假设之后，我们就能进行下一步的推导。&lt;/p&gt;
&lt;h2&gt;对数似然函数&lt;/h2&gt;
&lt;p&gt;在前面的先验假设中，我们需要用到 $\phi$ 、$\Sigma$ 、$\mu_0$ 和 $\mu_1$ 等参数，所以我们先要给出这些参数的参数估计。&lt;/p&gt;
&lt;p&gt;首先我们要求出对数似然函数。对于整个数据集，似然函数是：&lt;/p&gt;
&lt;p&gt;$$
\prod_{i=1}^m P(x^{(i)}, y^{(i)} \mid \phi, \Sigma, \mu_0, \mu_1) = \prod_{i=1}^m P(x^{(i)} \mid y^{(i)}) P(y^{(i)})
$$&lt;/p&gt;
&lt;p&gt;取对数后，得到的对数似然函数：&lt;/p&gt;
&lt;p&gt;$$
L(\phi, \Sigma, \mu_0, \mu_1) = \sum_{i=1}^m \left[ \log P(x^{(i)} \mid y^{(i)}) + \log P(y^{(i)}) \right]
$$&lt;/p&gt;
&lt;h3&gt;分解条件概率&lt;/h3&gt;
&lt;p&gt;为了便于处理不同类别的数据，我们将条件概率项 $\log P(x^{(i)} \mid y^{(i)})$ 根据 $y^{(i)}$ 的值进行分解。由于 $y^{(i)}$ 是二值的，我们可以使用 $y^{(i)}$ 作为指示函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 $y^{(i)} = 1$ 时，$\log P(x^{(i)} \mid y^{(i)}) = \log P(x^{(i)} \mid y^{(i)} = 1)$&lt;/li&gt;
&lt;li&gt;当 $y^{(i)} = 0$ 时，$\log P(x^{(i)} \mid y^{(i)}) = \log P(x^{(i)} \mid y^{(i)} = 0)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，求和项可以重写为：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^m \log P(x^{(i)} \mid y^{(i)}) = \sum_{i=1}^m \left[ y^{(i)} \log P(x^{(i)} \mid y=1) + (1-y^{(i)}) \log P(x^{(i)} \mid y=0) \right]
$$&lt;/p&gt;
&lt;p&gt;带入对数似然函数可得：&lt;/p&gt;
&lt;p&gt;$$
L(\phi, \Sigma, \mu_0, \mu_1) = \sum_{i=1}^m \left[ y^{(i)} \log P(x^{(i)} \mid y=1) + (1-y^{(i)}) \log P(x^{(i)} \mid y=0) \right] + \sum_{i=1}^m \log P(y^{(i)})
$$&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;高斯判别代码讲解&lt;/h1&gt;
&lt;p&gt;不同于前面两个模型，高斯判别模型是 &lt;strong&gt;闭式解&lt;/strong&gt; 的分类器，无需梯度下降之类的梯度迭代。因此高斯判别分析的代码非常简单，下面是完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
from sklearn.datasets import make_classification
X, y = make_classification(
    n_samples=500,
    n_features=5,
    n_classes=2,
    n_informative=5,
    n_redundant=0,
    random_state=42
)


class GDA:
    def __init__(self):
        self.phi = None
        self.mu0 = None
        self.mu1 = None
        self.sigma = None

    # 求解出四个关键参数
    def fit(self, X, y):
        m, _ = X.shape

        # 1. 计算先验概率 phi
        self.phi = np.mean(y)

        # 2. 计算各类别均值 mu0, mu1
        self.mu0 = np.mean(X[y == 0], axis=0)
        self.mu1 = np.mean(X[y == 1], axis=0)

        # 3. 向量化计算协方差矩阵 Sigma
        diff0 = X[y == 0] - self.mu0
        diff1 = X[y == 1] - self.mu1
        self.sigma = (diff0.T @ diff0 + diff1.T @ diff1) / m

    # 求解出线性判别函数的两个参数
    def predict_proba(self, X):
        inv_sigma = np.linalg.inv(self.sigma)
        
        # 线性判别函数参数
        w = inv_sigma @ (self.mu1 - self.mu0)

        b = (
              np.log(self.phi / (1 - self.phi))
            + 0.5 * self.mu0.T @ inv_sigma @ self.mu0
            - 0.5 * self.mu1.T @ inv_sigma @ self.mu1
        )
        
        return 1 / (1 + np.exp(-(X @ w + b)))

    def predict(self, X):
        return (self.predict_proba(X) &amp;gt;= 0.5).astype(int)


# 执行代码
if __name__ == &quot;__main__&quot;:    
    model = GDA()
    model.fit(X, y)
    y_pred = model.predict(X)
    print(&quot;准确率：&quot;, np.mean(y_pred == y))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 先验参数求解&lt;/h2&gt;
&lt;p&gt;接&lt;a href=&quot;#%E4%BC%BC%E7%84%B6%E5%87%BD%E6%95%B0-likelihood-function&quot;&gt;上面&lt;/a&gt;所说，我们要想满足先验，首先就要求解出四个关键参数。根据极大似然估计的原理，我们要想使得模型最优，就要让似然函数取到最大值，这等价于让对数似然函数取到最大值。因此我们可以对四个关键参数求偏导来得到四个参数的具体值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fit(self, X, y):
    m, _ = X.shape

    # 1. 计算先验概率 phi
    self.phi = np.mean(y)

    # 2. 计算各类别均值 mu0, mu1
    self.mu0 = np.mean(X[y == 0], axis=0)
    self.mu1 = np.mean(X[y == 1], axis=0)

    # 3. 向量化计算协方差矩阵 Sigma
    diff0 = X[y == 0] - self.mu0
    diff1 = X[y == 1] - self.mu1
    self.sigma = (diff0.T @ diff0 + diff1.T @ diff1) / m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据最大似然估计（MLE）的原理，我们对每个参数求偏导，并令其等于零即可解出参数的估计值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参数 $\phi$ 的估计&lt;/p&gt;
&lt;p&gt;参数 $\phi = P(y=1)$ 是类别标签 $y$ 的伯努利分布的参数。由于 $\phi$ 只存在于 $\displaystyle \sum_{i=1}^m \log P(y^{(i)})$ 一项中，我们可以单独对这一项求导：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\frac{\partial L(\phi, \Sigma, \mu_0, \mu_1)}{\partial \phi}
&amp;amp;= \frac{\partial \sum_{i=1}^m \log P(y^{(i)})}{\partial \phi} \
&amp;amp;= \frac{\partial \sum_{i=1}^m \log \phi^{y^{(i)}}(1 - \phi)^{1 - y^{(i)}}}{\partial \phi} \
&amp;amp;= \frac{\partial \sum_{i=1}^m y^{(i)} \log \phi + (1 - y^{(i)}) \log (1 - \phi)}{\partial \phi} \
&amp;amp;= \sum_{i=1}^m y^{(i)} \frac{1}{\phi} - (1 - y^{(i)}) \frac{1}{1 - \phi}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;令导数等于零，解得：&lt;/p&gt;
&lt;p&gt;$$
\phi = \frac{1}{m} \sum_{i=1}^m \frac{y^{(i)}}{y^{(i)} + (1 - y^{(i)})} = \frac{1}{m} \sum_{i=1}^m y^{(i)}
$$&lt;/p&gt;
&lt;p&gt;这正是 &lt;strong&gt;正样本在总样本中的比例&lt;/strong&gt; ，符合我们对先验概率 $\phi$ 的直观理解。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;均值向量 $\mu_k$ 的估计&lt;/p&gt;
&lt;p&gt;我们假设 $\mu_0$ 和 $\mu_1$ 分别是 $y=0$ 和 $y=1$ 时的条件均值向量。&lt;/p&gt;
&lt;p&gt;以 $\mu_1$ 为例，它只出现在条件概率 $P(x | y=1)$ 项中，因此有：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\frac{\partial L(\phi, \Sigma, \mu_0, \mu_1)}{\partial \mu_1}
&amp;amp;= \frac{\partial \sum_{i=1}^m y^{(i)} \log P(x^{(i)} | y^{(i)} = 1)}{\partial \mu_1} \
&amp;amp;= \frac{\partial \sum_{i=1}^m y^{(i)} \log \frac{1}{(2\pi)^{\frac{n}{2}} |\Sigma|^{\frac{1}{2}}} \exp(-\frac{1}{2}(x - \mu_1)^{\rm T} \Sigma^{-1}(x - \mu_1))}{\partial \mu_1} \
&amp;amp;= \sum_{i=1}^m y^{(i)} \Sigma^{-1}(x^{(i)} - \mu_1)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;令导数为零解得：&lt;/p&gt;
&lt;p&gt;$$
\mu_1 = \frac{\sum_{i=1}^m y^{(i)} x^{(i)}}{\sum_{i=1}^m y^{(i)}}
$$&lt;/p&gt;
&lt;p&gt;同理可得：&lt;/p&gt;
&lt;p&gt;$$
\mu_0 = \frac{\sum_{i=1}^m (1 - y^{(i)}) x^{(i)}}{\sum_{i=1}^m (1 - y^{(i)})}
$$&lt;/p&gt;
&lt;p&gt;这两个结果分别表示 &lt;strong&gt;正样本和负样本的样本均值&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;协方差矩阵 $\Sigma$ 的估计&lt;/p&gt;
&lt;p&gt;相对于上面三个参数，求解 $\Sigma$ 则更要复杂一些：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\frac{\partial L(\phi, \Sigma, \mu_0, \mu_1)}{\partial \Sigma}
&amp;amp;= \frac{\partial \sum_{i=1}^m y^{(i)} \log P(x^{(i)} | y^{(i)} = 1) + \sum_{i=1}^m (1 - y^{(i)}) \log P(x^{(i)} | y^{(i)} = 0)}{\partial \Sigma} \
&amp;amp;= \frac{\partial \sum_{i=1}^m \log \frac{1}{(2\pi)^{\frac{n}{2}} |\Sigma|^{\frac{1}{2}}} - \frac{1}{2} \sum_{i=1}^m (x^{(i)} - \mu_{y^{(i)}})^{\rm T} \Sigma^{-1}(x^{(i)} - \mu_{y^{(i)}})}{\partial \Sigma} \
&amp;amp;= \frac{\partial - \frac{m}{2} (n \log 2\pi + \log |\Sigma|) - \frac{1}{2} \sum_{i=1}^m (x^{(i)} - \mu_{y^{(i)}})^{\rm T} \Sigma^{-1}(x^{(i)} - \mu_{y^{(i)}})}{\partial \Sigma} \
&amp;amp;= - \frac{m}{2} \Sigma^{-1} - \frac{1}{2} \sum_{i=1}^m (x^{(i)} - \mu_{y^{(i)}})(x^{(i)} - \mu_{y^{(i)}})^{\rm T} (\Sigma^{-1})^2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;令等式为零并右乘 $\Sigma^2$ 解得：&lt;/p&gt;
&lt;p&gt;$$
\Sigma = \frac{1}{m} \sum_{i=1}^m (x^{(i)} - \mu_{y^{(i)}})(x^{(i)} - \mu_{y^{(i)}})^{\rm T}
$$&lt;/p&gt;
&lt;p&gt;其中 $\mu_{y^{(i)}}$ 表示 $x^{(i)}$ 所属类别的均值（即 $\mu_0$ 或 $\mu_1$ ）。&lt;/p&gt;
&lt;p&gt;这个结果正是 &lt;strong&gt;基于类别均值的总体协方差矩阵的无偏估计&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此我们就得到了所有参数的似然估计结果。&lt;/p&gt;
&lt;h2&gt;2. 模型参数求解&lt;/h2&gt;
&lt;p&gt;下面我们就来证明一下为什么 GDA 是 &lt;strong&gt;线性&lt;/strong&gt; 判别模型，以及具体的模型参数该如何求解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def predict_proba(self, X):
    inv_sigma = np.linalg.inv(self.sigma)
    
    # 线性判别函数参数
    w = inv_sigma @ (self.mu1 - self.mu0)

    b = (
            np.log(self.phi / (1 - self.phi))
        + 0.5 * self.mu0.T @ inv_sigma @ self.mu0
        - 0.5 * self.mu1.T @ inv_sigma @ self.mu1
    )
    
    return 1 / (1 + np.exp(-(X @ w + b)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GDA 的核心是计算后验概率，它可以通过贝叶斯定理得到：&lt;/p&gt;
&lt;p&gt;$$
P(y=1 | x) = \frac{P(x | y=1)P(y=1)}{P(x | y=0)P(y=0) + P(x | y=1)P(y=1)}
$$&lt;/p&gt;
&lt;p&gt;为了方便分类，我们用对数几率作为判别函数来求解：&lt;/p&gt;
&lt;p&gt;$$
\delta(x) = \log \frac{P(y = 1 | x)}{P(y = 0 | x)} = \log \frac{P(x | y = 1)P(y = 1)}{P(x | y = 0)P(y = 0)} = \log \frac{P(y=1)}{P(y=0)} + \log \frac{P(x | y=1)}{P(x | y=0)}
$$&lt;/p&gt;
&lt;h3&gt;线性模型推导&lt;/h3&gt;
&lt;p&gt;仔细观察&lt;a href=&quot;#%E5%85%88%E9%AA%8C%E5%81%87%E8%AE%BE-prior-assumptions&quot;&gt;上面&lt;/a&gt;的先验条件，我们假设了正例跟负例的协方差矩阵相同，因此条件概率密度公式可以写成：&lt;/p&gt;
&lt;p&gt;$$
P(x | y = k) = \frac{1}{(2\pi)^{\frac{n}{2}}|\Sigma|^{\frac{1}{2}}} \exp\left(-\frac{1}{2}(x-\mu_k)^{\rm T}\Sigma^{-1}(x-\mu_k)\right)
$$&lt;/p&gt;
&lt;p&gt;代入 $\delta(x)$ 的尾项后，展开平方化简可得：&lt;/p&gt;
&lt;p&gt;$$
\log\frac{P(x | y=1)}{P(x | y=0)} = - \frac{1}{2}(x - \mu_1)^{\rm T}\Sigma^{-1}(x - \mu_1) + \frac{1}{2}(x - \mu_0)^{\rm T}\Sigma^{-1}(x - \mu_0)
$$&lt;/p&gt;
&lt;p&gt;$$
\delta(x) = (\Sigma^{-1}(\mu_1 - \mu_0))^{\rm T}x + \left(\log \frac{\phi}{1 - \phi} - \frac{1}{2}(\mu_1^{\rm T}\Sigma^{-1}\mu_1 - \mu_0^{\rm T}\Sigma^{-1}\mu_0)\right)
$$&lt;/p&gt;
&lt;p&gt;对比线性判别函数的形式自然可以得到：&lt;/p&gt;
&lt;p&gt;$$
w = \Sigma^{-1}(\mu_1 - \mu_0)
$$&lt;/p&gt;
&lt;p&gt;$$
b = \log \frac{\phi}{1 - \phi} - \frac{1}{2}(\mu_1^{\rm T}\Sigma^{-1}\mu_1 - \mu_0^{\rm T}\Sigma^{-1}\mu_0)
$$&lt;/p&gt;
&lt;p&gt;根据这两个公式我们可以轻松求解高斯判别模型参数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;高斯判别分析要求正例和负例满足一定的条件，其中一个便是协方差要求相同，这是为什么？&lt;/p&gt;
&lt;p&gt;标准的 GDA（也是最常见的一种形式）假设：&lt;/p&gt;
&lt;p&gt;$$
\Sigma_0 = \Sigma_1 = \Sigma
$$&lt;/p&gt;
&lt;p&gt;这意味着：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不同类别的样本具有 &lt;strong&gt;相同&lt;/strong&gt; 的协方差矩阵，只是均值不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个假设非常重要，因为它会带来下面的结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线性判别分析（Linear Discriminant Analysis，简称 LDA）&lt;/strong&gt;：当我们假设 $\Sigma_0 = \Sigma_1 = \Sigma$ 时，对数几率 $\delta(x)$ 中的二次项（如 $x^{\rm T} \Sigma^{-1} x$ ）会被抵消，判别边界 $w^{\rm T} x + b = 0$ 是一条直线（或超平面）。此时 GDA 的决策边界与逻辑回归（Logistic Regression）类似，因此在决策边界层面 GDA 与 LDA 高度相关。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;二次判别分析（Quadratic Discriminant Analysis，简称 QDA）&lt;/strong&gt;：如果我们 &lt;strong&gt;不强制&lt;/strong&gt; 协方差相等，即假设 $\Sigma_0 \ne \Sigma_1$ ，那么对数几率 $\delta(x)$ 中涉及 $x$ 的二次项将 &lt;strong&gt;不会被抵消&lt;/strong&gt; 。此时 $\delta(x)$ 会出现 $x^{\rm T} A x$ 形式的二次项，导致决策边界是一条 &lt;strong&gt;二次曲线/曲面&lt;/strong&gt; 。这种模型被称为二次判别分析。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高斯判别分析与逻辑回归的区别是什么？&lt;/p&gt;
&lt;p&gt;由上面证明 “为什么 GDA 是线性判别模型” 的结果我们可以知道，高斯判别模型的对数几率函数只不过是一个复杂的线性函数罢了，因此高斯判别模型本质上就是逻辑回归。&lt;strong&gt;无需迭代但需要一定的先验&lt;/strong&gt; 便是高斯判别模型跟逻辑回归的区别，局限（需要先验）但高效（无需迭代）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/LuckyGlass-blog/p/17159433.html&quot;&gt;高斯判别分析GDA推导与代码实现&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cometeme.github.io/ml/2019/09/ML-GDA%E9%AB%98%E6%96%AF%E5%88%A4%E5%88%AB%E5%88%86%E6%9E%90.html&quot;&gt;[ML] GDA 高斯判别分析&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习笔记】数据处理 (Google ML)</title><link>https://xingguang641.com/posts/ml-note/data-processing/</link><guid isPermaLink="true">https://xingguang641.com/posts/ml-note/data-processing/</guid><description>基于 Google ML 课程的数据处理笔记</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数据类型辨析&lt;/h1&gt;
&lt;p&gt;在机器学习中，正确识别数据类型是选择处理方法的前提。&lt;/p&gt;
&lt;h3&gt;数值数据（Numerical Data）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：表示数量、大小或度量的连续或离散数值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：具有 &lt;strong&gt;可度量性&lt;/strong&gt;（Measurable）和 &lt;strong&gt;有序性&lt;/strong&gt;（Ordered）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运算&lt;/strong&gt;：支持数学运算（如加减乘除），其数值大小具有实际的物理意义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：温度、体重、商品价格、鹿群数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分类数据（Categorical Data）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：表示某种特征的离散类别或标签。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：通常 &lt;strong&gt;无序&lt;/strong&gt;（Nominal）且 &lt;strong&gt;不可累加&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理&lt;/strong&gt;：机器无法直接理解文本标签，通常需要转化为数值形式，常用方法包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;整数编码（Label Encoding）&lt;/strong&gt;：用于有序类别（如：低/中/高 -&amp;gt; 0/1/2）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独热编码（One-Hot Encoding）&lt;/strong&gt;：用于无序类别（如：红/绿/蓝）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：性别、颜色、省份、犬种。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
&lt;strong&gt;⚠️ 易错点：数字不一定都是数值数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;美国邮政编码（ZIP Code）&lt;/strong&gt; 虽然由数字组成（如 &lt;code&gt;20002&lt;/code&gt;, &lt;code&gt;40004&lt;/code&gt;），但它们属于 &lt;strong&gt;分类数据&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理由&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;无数学意义&lt;/strong&gt;：邮编 &lt;code&gt;40004&lt;/code&gt; 并不是邮编 &lt;code&gt;20002&lt;/code&gt; 的两倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仅作标识&lt;/strong&gt;：数字在这里仅代表地理区域的 “ID” ，而非数量的大小。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;处理建议&lt;/strong&gt;：在特征工程中，应将邮政编码视为离散的类别特征进行独热编码或其他嵌入处理，而不是直接作为连续数值输入模型。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;特征工程&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;核心问题&lt;/strong&gt;：直接使用原始数据（Raw Data）进行训练效果最好吗？&lt;strong&gt;答案&lt;/strong&gt;：通常不是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特征工程&lt;/strong&gt; 是将原始数据转换为更能代表潜在问题预测模型的特征的过程。它是机器学习成功的关键要素。&lt;/p&gt;
&lt;p&gt;我们必须将原始数据转换为 &lt;strong&gt;特征向量（Feature Vector）&lt;/strong&gt;。常见的预处理技术包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;归一化（Normalization）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将数值缩放到统一的范围（如 [0, 1] 或 [-1, 1]）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：防止某些大数值特征主导梯度下降的方向，加速收敛。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分箱（Binning/Bucketing）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将连续数值划分为若干个离散的区间（桶）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：引入非线性，降低离群值的影响。例如，将 “年龄” 划分为 “青年/中年/老年” 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;延伸阅读&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/166356924&quot;&gt;知乎：特征工程详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_55948984/article/details/136402828&quot;&gt;CSDN：特征工程 9 大方法&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;📚 学习资源汇总&lt;/h1&gt;
&lt;h2&gt;数据可视化&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;数据可视化是理解数据分布、发现异常值的首要步骤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;&quot;&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=660383281&amp;amp;bvid=BV1dh4y127Pv&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113991753998159&amp;amp;bvid=BV1doK7eJEfg&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=426551834&amp;amp;bvid=BV1t3411A7Z8&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=795023367&amp;amp;bvid=BV1YC4y147cE&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;数据预处理实战&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;涵盖特征工程、数据清洗及 PyTorch 工程规范。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;&quot;&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=976027811&amp;amp;bvid=BV1t44y1x7Hw&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=115254222656103&amp;amp;bvid=BV1DtJyz6EJv&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114478410699738&amp;amp;bvid=BV1VrVSz1Eme&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114561508187738&amp;amp;bvid=BV15kj4z4Eju&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;Kaggle 经典项目实战系列&lt;/h3&gt;
&lt;p&gt;本系列博客完整记录了从数据理解到模型训练的全流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据探索&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/81366353/&quot;&gt;数据理解与整体探索&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据清洗&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/81542011/&quot;&gt;数据清洗实战&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征变换&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/81583734/&quot;&gt;特征转换与衍生&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征筛选&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/81911511/&quot;&gt;特征筛选策略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型训练&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/81913016/&quot;&gt;模型训练与调优&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程规范&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/wsp_1138886114/article/details/87911264/&quot;&gt;PyTorch 工程规范指南&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;进阶阅读&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/deepever/article/details/148565284&quot;&gt;AI 大模型中的数据清洗与预处理技术研究&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>【机器学习笔记】回归分析 (Google ML)</title><link>https://xingguang641.com/posts/ml-note/regression-analysis/</link><guid isPermaLink="true">https://xingguang641.com/posts/ml-note/regression-analysis/</guid><description>基于 Google ML 课程的回归分析学习笔记</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线性回归&lt;/h1&gt;
&lt;h2&gt;1. 损失度量指标&lt;/h2&gt;
&lt;p&gt;在回归任务中，我们通常使用以下几种指标来衡量模型的预测误差：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MAE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;平均绝对误差（Mean Absolute Error）&lt;/td&gt;
&lt;td&gt;对离群值不敏感，反映预测值的平均偏移量。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;均方误差（Mean Squared Error）&lt;/td&gt;
&lt;td&gt;对离群值敏感（误差被平方放大），便于梯度计算。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RMSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;均方根误差（Root Mean Squared Error）&lt;/td&gt;
&lt;td&gt;量纲与原目标变量一致，便于直观解释。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2. 选择损失函数&lt;/h2&gt;
&lt;p&gt;损失函数的选择直接决定了模型如何处理 &lt;strong&gt;离群值（Outliers）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;L2 损失 (MSE)&lt;/strong&gt;：由于误差被平方，大的误差会产生巨大的损失值，因此模型会更关注离群点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L1 损失 (MAE)&lt;/strong&gt;：误差呈线性增长，模型对离群点的容忍度更高，拟合结果更具鲁棒性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::important
&lt;strong&gt;💡 决策指南&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择 MSE 的场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主要考量&lt;/strong&gt;：你需要对大的预测误差施加严厉惩罚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据特征&lt;/strong&gt;：离群值代表了重要的数据分布信息（而非噪声），模型必须尽可能拟合它们。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数学优势&lt;/strong&gt;：MSE 是处处可导的凸函数，优化过程通常比 MAE 更平滑、收敛更稳健。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择 MAE 的场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主要考量&lt;/strong&gt;：你希望模型具有鲁棒性，不受少量极端值的干扰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据特征&lt;/strong&gt;：数据集中包含噪声或异常值，且你不希望这些异常点主导模型的训练方向。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 梯度下降&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;可视化演示：梯度下降如何寻找最优解&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114052319679117&amp;amp;bvid=BV1CVAUeuECE&amp;amp;cid=28536998241&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;4. 模型收敛问题&lt;/h2&gt;
&lt;p&gt;线性回归模型的损失函数（通常是 MSE）是一个 &lt;strong&gt;凸函数（Convex Function）&lt;/strong&gt;。这意味着：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;损失曲线形状如碗状，函数图像 &lt;strong&gt;仅存在一个全局最小值&lt;/strong&gt; ，不存在局部极小值陷阱。&lt;/li&gt;
&lt;li&gt;只要学习率设置合理，梯度下降法理论上一定能收敛到全局最优解（权重与偏置）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 超参数&lt;/h2&gt;
&lt;p&gt;超参数是训练前人为设定的 “旋钮” ，它们不通过数据训练得到，但决定了模型的结构和训练过程。&lt;/p&gt;
&lt;h3&gt;训练动态类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;学习率（Learning Rate）&lt;/strong&gt;：梯度下降的步长。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;过大&lt;/em&gt;：可能导致震荡或发散。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;过小&lt;/em&gt;：收敛速度极慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批大小（Batch Size）&lt;/strong&gt;：单次参数更新所使用的样本数量。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;大 Batch&lt;/em&gt;：梯度估计更准，训练更稳，但显存要求高。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;小 Batch&lt;/em&gt;：引入随机性，有助于跳出局部最优（在非凸优化中），但训练波动大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代轮次（Epochs）&lt;/strong&gt;：模型遍历完整训练集的次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化器（Optimizer）&lt;/strong&gt;：参数更新的策略算法（如 SGD, Adam, RMSProp）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模型结构与正则化类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正则化系数（Regularization Strength）&lt;/strong&gt;：控制正则项（L1/L2）的权重，用于平衡偏差与方差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏层与神经元（Hidden Layers &amp;amp; Units）&lt;/strong&gt;：(针对神经网络) 决定模型的容量和非线性表达能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激活函数 (Activation Function)&lt;/strong&gt;：（针对神经网络）引入非线性因素（如 ReLU, Sigmoid）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dropout 率&lt;/strong&gt;：随机失活神经元的比例，用于防止过拟合。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;逻辑回归&lt;/h1&gt;
&lt;h2&gt;1. 对数几率回归&lt;/h2&gt;
&lt;p&gt;逻辑回归的核心是将线性输出映射到 $(0, 1)$ 区间，以表示概率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;S 型函数（Sigmoid Function）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\sigma(z) = \frac{1}{1 + e^{-z}}
$$&lt;/p&gt;
&lt;p&gt;它将任意实数 $z$ 压缩到 $(0, 1)$ 之间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对数几率（Logit Function）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它是 Sigmoid 的反函数，定义为概率 $p$ 的对数几率：&lt;/p&gt;
&lt;p&gt;$$
\text{logit}(p) = \ln\left(\frac{p}{1 - p}\right)
$$&lt;/p&gt;
&lt;p&gt;两者互为逆运算：$\text{logit}(\sigma(x)) = x$。这解释了逻辑回归也被称为 “对数几率回归” 的原因。&lt;/p&gt;
&lt;h2&gt;2. 损失与正则化&lt;/h2&gt;
&lt;p&gt;:::tip
&lt;strong&gt;逻辑回归 vs 线性回归&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虽然逻辑回归在形式上看似只是线性回归加了一个 Sigmoid 壳，但它们的训练核心有两点本质不同：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;损失函数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性回归&lt;/strong&gt; 使用 &lt;strong&gt;平方损失（Squared Loss）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑回归&lt;/strong&gt; 使用 &lt;strong&gt;对数损失（Log Loss / Cross Entropy）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;原因&lt;/em&gt;：如果在逻辑回归中使用平方损失，损失函数将变为非凸函数，难以优化；而对数损失不仅是凸函数，还基于最大似然估计推导而来，物理意义明确。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;正则化的必要性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逻辑回归极易在处理高维特征或线性可分数据时发生 &lt;strong&gt;过拟合&lt;/strong&gt;（权重趋向无穷大以使概率逼近 1 或 0）。&lt;/li&gt;
&lt;li&gt;因此引入 &lt;strong&gt;L2（Ridge）&lt;/strong&gt; 或 &lt;strong&gt;L1（Lasso）&lt;/strong&gt; 正则化在逻辑回归中几乎是标准的做法。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;📚 学习资源汇总&lt;/h1&gt;
&lt;h2&gt;回归分析系列&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下视频涵盖了从线性回归基础到进阶实战的完整流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;&quot;&amp;gt;
&amp;lt;!-- 建议：如果不希望页面太长，可以使用这种 Grid 布局，或者只保留 1-2 个核心视频，其他的放链接 --&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113794554528405&amp;amp;bvid=BV1shriYXEEv&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113794571308923&amp;amp;bvid=BV1s8riYWEVK&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113794588084047&amp;amp;bvid=BV1periYDE65&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113913471435418&amp;amp;bvid=BV18KF3eBEwD&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114584929177246&amp;amp;bvid=BV1hsjqzUETa&amp;amp;&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114604038425646&amp;amp;bvid=BV1MK7MzAEGN&amp;amp;&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114649773114756&amp;amp;bvid=BV1CJTdzaEMa&amp;amp;&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114688394268381&amp;amp;bvid=BV1XFM8zVECm&amp;amp;&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;实战项目与代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性回归实战&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/qq_41750911/article/details/124883520&quot;&gt;CSDN - 机器学习之线性回归算法 (Linear Regression)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑回归实战&lt;/strong&gt;：&lt;a href=&quot;https://blog.csdn.net/weixin_50744311/article/details/131523136&quot;&gt;CSDN - Logistic回归及Python代码实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;综合实战&lt;/strong&gt;：&lt;a href=&quot;https://www.bilibili.com/video/BV1Jd4y1T7rw/&quot;&gt;Bilibili - 两天搞定AI毕设：构建自己的图像分类数据集&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>【机器学习基本模型】第一节：线性回归</title><link>https://xingguang641.com/posts/regression-model/linear-regression/</link><guid isPermaLink="true">https://xingguang641.com/posts/regression-model/linear-regression/</guid><description>介绍机器学习常见的模型</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线性回归基本原理&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;线性回归（Linear Regression）&lt;/strong&gt; 是机器学习中最基础、最经典的算法之一。尽管其结构简单，但它揭示了从数据中学习规律的核心思想，是学习更复杂算法（如神经网络）的基石。&lt;/p&gt;
&lt;p&gt;其核心思想非常直观：&lt;strong&gt;通过拟合一个线性函数，来刻画输入变量与输出变量之间的定量关系&lt;/strong&gt; 。通俗地说，线性回归试图在数据点中找到一条 “最佳拟合直线（或超平面）” ，使得所有样本点到这条直线的 “综合距离” 最小，从而能够根据新的输入预测出合理的输出。&lt;/p&gt;
&lt;h2&gt;从简单到多元&lt;/h2&gt;
&lt;p&gt;在最简单的情况下，我们只有一个输入特征 $x$ 。例如仅根据 “房屋面积” 来预测 “房价” ，模型的形式就是最熟悉的直线方程：&lt;/p&gt;
&lt;p&gt;$$
y = wx + b
$$&lt;/p&gt;
&lt;p&gt;当输入变量扩展到多个维度时（例如根据 “面积” 、“房龄” 、“距离地铁距离” 共同预测 “房价” ），这种思想就自然地推广为 &lt;strong&gt;多元线性回归（Multiple Linear Regression）&lt;/strong&gt;。此时模型的目标是找到一个函数，让它能够描述输入向量 $x = [x_1, x_2, \ldots, x_n]^{\rm T}$ 与输出变量 $y$ 之间的关系：&lt;/p&gt;
&lt;p&gt;$$
y = w^{\rm T} x + b
$$&lt;/p&gt;
&lt;p&gt;其中 $w = [w_1, w_2, \ldots, w_n]^{\rm T}$ 是权重向量（Weight），表示每个特征的重要性。$b$ 是偏置项（Bias），表示截距。从几何角度看，这个模型对应于 $n$ 维空间中的一个 &lt;strong&gt;超平面（Hyperplane）&lt;/strong&gt;。训练模型的过程，本质上就是不断调整 $w$ 和 $b$ ，使这个超平面尽可能贴近所有训练数据点，从而最小化预测误差。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cregression-model%5C%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92%E5%88%86%E6%9E%901.jpg&quot; alt=&quot;线性回归图像&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;线性回归代码讲解&lt;/h1&gt;
&lt;p&gt;为了便于理解和可视化，下面的代码展示了 &lt;strong&gt;简单线性回归（单特征）&lt;/strong&gt; 的基本实现过程（建议先通读一遍代码以建立整体印象，若有细节暂时不理解，可在阅读后文的 “原理讲解” 部分时再回头对照理解）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
data = np.array([
    [32, 31], [53, 68], [61, 62], [47, 71], [59, 87],
    [55, 78], [52, 79], [39, 59], [48, 75], [52, 71],
    [45, 55], [54, 82], [44, 62], [58, 75], [56, 81],
    [48, 60], [44, 82], [60, 97], [45, 48], [38, 56],
    [66, 83], [65, 118], [47, 57], [41, 51], [51, 75],
    [59, 74], [57, 95], [63, 95], [46, 79], [50, 83]
])


# 损失函数
def loss_func(w, b, data):
    total_cost = 0
    for i in range(len(data)):
        x, y = data[i]
        total_cost += (w * x + b - y) ** 2
    return total_cost / len(data)
# 梯度下降
def grad_desc(cur_w, cur_b, alpha, data):
    sum_w = 0
    sum_b = 0
    # 对每个点，代入公式求和
    for i in range(len(data)):
        x, y = data[i]
        sum_w += (cur_w * x + cur_b - y) * x
        sum_b += cur_w * x + cur_b - y
    # 用公式求当前梯度
    grad_w = 2 / len(data) * sum_w
    grad_b = 2 / len(data) * sum_b
    # 梯度下降，更新当前的w和b
    updated_w = cur_w - alpha * grad_w
    updated_b = cur_b - alpha * grad_b
    return updated_w, updated_b
# 主函数
def main(data, initial_w, initial_b, alpha, num_iter):
    w = initial_w
    b = initial_b
    # 定义一个list保存所有的损失函数值，用来显示下降的过程
    cost_list = []
    for i in range(num_iter):
        cost_list.append(loss_func(w, b, data))
        w, b = grad_desc(w, b, alpha, data)
    return [w, b, cost_list]


# 设置超参数
alpha = 0.0001
initial_w = 0
initial_b = 0
num_iter = 10
# 执行代码
if __name__ == &quot;__main__&quot;:
    w, b, cost_list = main(data, initial_w, initial_b, alpha, num_iter)
    print(&quot;\n训练结束&quot;)
    print(&quot;w =&quot;, w)
    print(&quot;b =&quot;, b)
    cost = loss_func(w, b, data)
    print(&quot;cost =&quot;, cost)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 损失函数&lt;/h2&gt;
&lt;p&gt;如何衡量模型预测的准确性？我们需要一个评估指标。在线性回归中，最常用的指标是 &lt;strong&gt;均方误差&lt;/strong&gt;（Mean Squared Error，简称 MSE）。下面给出对应的代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def loss_func(w, b, data):
    total_cost = 0
    for i in range(len(data)):
        x, y = data[i]
        total_cost += (w * x + b - y) ** 2
    return total_cost / len(data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数 &lt;code&gt;loss_func&lt;/code&gt; 对应的数学公式为：&lt;/p&gt;
&lt;p&gt;$$
L(w, b) = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$y_i$ ：第 $i$ 个样本的真实标签。&lt;/li&gt;
&lt;li&gt;$\hat{y}_i = w x_i + b$ ：模型对第 $i$ 个样本的预测值。&lt;/li&gt;
&lt;li&gt;$(y_i - \hat{y}_i)^2$ ：预测误差的平方。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 梯度下降&lt;/h2&gt;
&lt;p&gt;在有了损失函数之后，我们的目标就是找到使损失最小的最优参数 $w$ 和 $b$ 。当解析解不可行或特征维度较高时，最常用的方法便是 &lt;strong&gt;梯度下降（Gradient Descent）&lt;/strong&gt;———— 根据损失函数的梯度不断更新参数，就像沿着山坡的最陡方向一步步 “下山” 。&lt;/p&gt;
&lt;p&gt;参数更新公式为：&lt;/p&gt;
&lt;p&gt;$$
w \leftarrow w - \alpha \frac{\partial L}{\partial w} \quad\quad b \leftarrow b - \alpha \frac{\partial L}{\partial b}
$$&lt;/p&gt;
&lt;p&gt;其中 $\alpha$ 是 &lt;strong&gt;学习率（Learning Rate）&lt;/strong&gt;，用于控制每一步更新的幅度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def grad_desc(cur_w, cur_b, alpha, data):
    sum_w = 0
    sum_b = 0
    # 对每个样本点，累加梯度
    for i in range(len(data)):
        x, y = data[i]
        sum_w += (cur_w * x + cur_b - y) * x
        sum_b += cur_w * x + cur_b - y
    # 计算当前梯度
    grad_w = 2 / len(data) * sum_w
    grad_b = 2 / len(data) * sum_b
    # 更新参数
    updated_w = cur_w - alpha * grad_w
    updated_b = cur_b - alpha * grad_b
    return updated_w, updated_b
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;梯度推导&lt;/h3&gt;
&lt;p&gt;令误差项 $e_i = (w x_i + b) - y_i$ ，损失函数可以写为：&lt;/p&gt;
&lt;p&gt;$$
L = \frac{1}{n}\sum e_i^2
$$&lt;/p&gt;
&lt;p&gt;根据链式法则，可以得到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对 $w$ 求偏导&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial w} = \frac{1}{n} \sum_{i=1}^{n} 2 e_i \cdot \frac{\partial e_i}{\partial w} = \frac{2}{n} \sum_{i=1}^{n} \underbrace{((w x_i + b) - y_i)}_{\text{误差}} \cdot x_i
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对 $b$ 求偏导&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\frac{\partial L}{\partial b} = \frac{1}{n} \sum_{i=1}^{n} 2 e_i \cdot \frac{\partial e_i}{\partial b} = \frac{2}{n} \sum_{i=1}^{n} ((w x_i + b) - y_i)
$$&lt;/p&gt;
&lt;p&gt;代码中的更新规则正是对上述推导的直接实现。&lt;/p&gt;
&lt;h2&gt;3. 内容拓展&lt;/h2&gt;
&lt;p&gt;虽然在本示例中使用梯度下降进行训练，但对于线性回归这种凸优化问题，其实可以通过 &lt;strong&gt;最小二乘法（Least Squares）&lt;/strong&gt; 直接求得参数的闭式解，也就是所谓的 &lt;strong&gt;解析解（Analytical Solution）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;统计视角下的推导&lt;/h3&gt;
&lt;p&gt;假设真实数据生成过程为：&lt;/p&gt;
&lt;p&gt;$$
y_i = \beta_0 + \beta_1 x_i + \varepsilon_i
$$&lt;/p&gt;
&lt;p&gt;其中 $\varepsilon_i$ 是随机噪声。&lt;/p&gt;
&lt;p&gt;为了让模型拟合效果最佳，我们希望最小化残差平方和（SSE）：&lt;/p&gt;
&lt;p&gt;$$
J(\beta_0, \beta_1) = \sum_{i=1}^{n} \big(y_i - (\beta_0 + \beta_1 x_i)\big)^2
$$&lt;/p&gt;
&lt;p&gt;这是一个求极值的问题。通过对 $\beta_0$ 和 $\beta_1$ 分别求偏导并令其为 0，可以得到最优参数的闭式解：&lt;/p&gt;
&lt;p&gt;$$
\hat{\beta}&lt;em&gt;1 = \frac{\sum&lt;/em&gt;{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y})}{\sum_{i=1}^{n} (x_i - \bar{x})^2}
$$&lt;/p&gt;
&lt;p&gt;$$
\hat{\beta}_0 = \bar{y} - \hat{\beta}_1 \bar{x}
$$&lt;/p&gt;
&lt;p&gt;（注：这里的 $\beta_1$ 对应前文的 $w$ ，$\beta_0$ 对应前文的 $b$ ）&lt;/p&gt;
&lt;h3&gt;为什么仍需要梯度下降？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算复杂度&lt;/strong&gt;：解析解需要计算矩阵的逆（尤其在多元回归中），当特征维度很高时，计算量会非常大。而梯度下降通过迭代逼近最优解，在海量数据场景下更加高效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用性&lt;/strong&gt;：大多数复杂的机器学习模型（如深度神经网络）没有解析解，必须依赖梯度下降或其变种进行优化。因此在线性回归中学习梯度下降，不仅能理解优化机制，也为后续复杂模型的学习打下基础。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_41750911/article/details/124883520&quot;&gt;机器学习之线性回归算法 Linear Regression&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://otexts.com/fppcn/least-squares.html&quot;&gt;【方法与实践】最小二乘估计讲解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【机器学习基本模型】第二节：逻辑回归</title><link>https://xingguang641.com/posts/regression-model/logistic-regression/</link><guid isPermaLink="true">https://xingguang641.com/posts/regression-model/logistic-regression/</guid><description>介绍机器学习常见的模型</description><pubDate>Thu, 23 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;逻辑回归基本原理&lt;/h1&gt;
&lt;p&gt;在上一节的线性回归中，我们解决的是回归问题（预测连续值）。而在模式识别与机器学习中，我们更常遇到的是 &lt;strong&gt;分类任务&lt;/strong&gt; ，例如判断一封邮件是否为垃圾邮件，或者判断一个人是否患有某种疾病。&lt;/p&gt;
&lt;p&gt;对于这类二分类问题，输出标签通常为 $y \in {0, 1}$ 。如果我们直接使用线性回归模型预测，输出值可能会远超 0 到 1 的范围，这在概率解释上是不合理的。为此，我们在线性模型的基础上引入了一个非线性激活函数 $g: \mathbb{R} \to (0,1)$ ，将线性预测值映射为类别标签的后验概率 $P(y = 1 | \mathbf{x})$ 。&lt;/p&gt;
&lt;h2&gt;对数几率回归&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;逻辑回归（Logistic Regression）&lt;/strong&gt; 中，选用的激活函数为 &lt;strong&gt;Sigmoid 函数&lt;/strong&gt; ，其表达式为：&lt;/p&gt;
&lt;p&gt;$$
\sigma(z) = \frac{1}{1 + e^{-z}}
$$&lt;/p&gt;
&lt;p&gt;模型的预测目标是样本属于正类（ $y=1$ ）的后验概率：&lt;/p&gt;
&lt;p&gt;$$
P(y = 1 | \mathbf{x}) = \sigma(\mathbf{w}^{\rm T} \mathbf{x}) = \frac{1}{1 + e^{-\mathbf{w}^{\rm T} \mathbf{x}}}
$$&lt;/p&gt;
&lt;p&gt;为了简化公式，我们通常采用 &lt;strong&gt;增广向量&lt;/strong&gt; 的形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增广特征向量 $\mathbf{x} = [x_1, \cdots, x_D, 1]^{\rm T}$&lt;/li&gt;
&lt;li&gt;增广权重向量 $\mathbf{w} = [w_1, \cdots, w_D, b]^{\rm T}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，样本属于负类（ $y=0$ ）的后验概率为：&lt;/p&gt;
&lt;p&gt;$$
P(y=0 | \mathbf{x}) = 1 - P(y=1 | \mathbf{x})
= 1 - \sigma(\mathbf{w}^{\rm T} \mathbf{x})
= \frac{e^{- \mathbf{w}^{\rm T} \mathbf{x}}}{1 + e^{- \mathbf{w}^{\rm T} \mathbf{x}}}
$$&lt;/p&gt;
&lt;p&gt;通过推导，我们可以发现线性模型 $\mathbf{w}^{\rm T} \mathbf{x}$ 与概率之间的关系：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{w}^{\rm T} \mathbf{x}
= \ln \frac{P(y=1 | \mathbf{x})}{1 - P(y=1 | \mathbf{x})}
= \ln \frac{P(y=1 | \mathbf{x})}{P(y=0 | \mathbf{x})}
$$&lt;/p&gt;
&lt;p&gt;该公式左侧为典型的 &lt;strong&gt;线性函数&lt;/strong&gt; ，右侧则是将后验概率之比（数学上称为 &lt;strong&gt;几率&lt;/strong&gt; ，Odds）通过对数变换映射至实数域，这种建立线性预测值与 &lt;strong&gt;对数几率&lt;/strong&gt; 之间映射关系的方法，也正是 Logistic 回归被称为 &lt;strong&gt;对数几率回归&lt;/strong&gt; 的由来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src%5Ccontent%5Cposts%5Cregression-model%5C%E9%80%BB%E8%BE%91%E5%9B%9E%E5%BD%92%E5%88%86%E6%9E%901.jpg&quot; alt=&quot;逻辑回归图像&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;逻辑回归代码讲解&lt;/h1&gt;
&lt;p&gt;下面我们使用 Python 的 NumPy 库来手写实现一个逻辑回归模型。为了让代码结构更加紧凑，并能够直接表达数学公式的含义，示例中会用到 &lt;strong&gt;匿名函数（Lambda Function）&lt;/strong&gt; 来定义核心运算。&lt;/p&gt;
&lt;p&gt;值得注意的是，为了与前面理论部分所使用的 &lt;strong&gt;增广向量（Augmented Vector）&lt;/strong&gt; 保持一致，我们会在输入特征矩阵 $X$ 的最左侧额外添加一列常数 1，从而把偏置项 $b$ 吸收到权重向量 $w$ 中统一处理。这样不仅使参数更新的形式更加整齐，也顺便修复了原版本代码中函数返回值与参数解包数量不匹配的问题，使整体实现更加规范和可靠。&lt;/p&gt;
&lt;p&gt;下面给出逻辑回归的完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
np.random.seed(0)
X = np.random.randn(100, 2)
true_w = np.array([2, -1])
sigmoid = lambda x: 1 / (1 + np.exp(-x))
y = (sigmoid(X @ true_w) &amp;gt; 0.5).astype(int)


# 激活函数
sigmoid = lambda x: 1 / (1 + np.exp(-x))
# 损失函数
loss_func = lambda X, y, w: -np.mean(
    y * np.log(sigmoid(X @ w)) + (1 - y) * np.log(1 - sigmoid(X @ w))
)
# 梯度下降
gradient = lambda X, y, w: X.T @ (sigmoid(X @ w) - y) / len(y)
def grad_desc(cur_w, alpha, X, y):
    grad = gradient(X, y, cur_w)
    updated_w = cur_w - alpha * grad
    return updated_w
# 主函数
def main(X, y, initial_w, alpha, num_iter):
    w = initial_w
    # 定义一个list保存所有的损失函数值，用来显示下降的过程
    cost_list = []
    for i in range(num_iter):
        cost_list.append(loss_func(X, y, w))
        w, b = grad_desc(w, alpha, X, y)
    return [w, b, cost_list]


# 设置超参数
alpha = 0.1
initial_w = np.zeros(X.shape[1])
num_iter = 1000
# 执行代码
if __name__ == &quot;__main__&quot;:
    w, cost_list = main(X, y, initial_w, alpha, num_iter)
    print(&quot;\n训练结束&quot;)
    print(&quot;w =&quot;, w)
    cost = loss_func(X, y, w)
    print(&quot;cost =&quot;, cost)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 损失函数&lt;/h2&gt;
&lt;p&gt;与线性回归使用均方误差（MSE）不同，逻辑回归采用的是 &lt;strong&gt;交叉熵损失（Cross-Entropy Loss）&lt;/strong&gt;。其核心原因在于：如果将 Sigmoid 函数套入 MSE 中，所得损失函数在参数空间里会变成一个 &lt;strong&gt;非凸函数（Non-Convex）&lt;/strong&gt;，可能具有多个局部极小值，从而使梯度下降难以稳定地找到全局最优解。相比之下，交叉熵损失在逻辑回归的模型结构下是一个凸函数，具备更好的优化特性，因此成为逻辑回归的标准选择。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多细节可以参考以下视频深入了解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=114675626875309&amp;amp;bvid=BV12VMzzxExF&amp;amp;cid=30475028383&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;先来看一下代码中的损失函数实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loss_func = lambda X, y, w: -np.mean(
    y * np.log(sigmoid(X @ w)) + (1 - y) * np.log(1 - sigmoid(X @ w))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的数学表达式为交叉熵损失函数：&lt;/p&gt;
&lt;p&gt;$$
J(\mathbf{w}) = -\frac{1}{N} \sum_{i=1}^{N} \Big[ y_i \ln(\hat{y}_i) + (1 - y_i)\ln(1 - \hat{y}_i) \Big]
$$&lt;/p&gt;
&lt;p&gt;其中 $\hat{y}_i = \sigma(\mathbf{x}_i \mathbf{w})$ 表示模型对第 $i$ 个样本的预测概率。&lt;/p&gt;
&lt;h2&gt;2. 梯度下降&lt;/h2&gt;
&lt;p&gt;逻辑回归在模型形式上与线性回归非常相似，只是在预测输出上多了一层 &lt;strong&gt;Sigmoid 激活函数&lt;/strong&gt; 。从微积分角度来看，这仅仅增加了链式法则中的一个环节。代码中梯度计算的实现非常简洁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gradient = lambda X, y, w: X.T @ (sigmoid(X @ w) - y) / len(y)

def grad_desc(cur_w, alpha, X, y):
    grad = gradient(X, y, cur_w)
    updated_w = cur_w - alpha * grad
    return updated_w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其数学推导如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预测值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\hat{y} = \sigma(X\mathbf{w}) = \frac{1}{1 + e^{-X\mathbf{w}}}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;损失函数&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
J(\mathbf{w}) = -\frac{1}{N} \sum_{i=1}^{N} \Big[ y_i \ln(\hat{y}_i) + (1 - y_i)\ln(1 - \hat{y}_i) \Big]
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;权重梯度&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\nabla J(\mathbf{w}) = \frac{\partial J(\mathbf{w})}{\partial \mathbf{w}}
= \frac{1}{N} X^{\rm T} (\hat{y} - y)
$$&lt;/p&gt;
&lt;p&gt;可以看到，这一梯度推导的最终形式与线性回归在结构上几乎一致，只是将预测值替换为逻辑回归中的 $\hat{y}$ 。正因为两者在形式上的高度相似，逻辑回归也被视为 &lt;strong&gt;广义线性模型（GLM）&lt;/strong&gt; 的一个典型特例。&lt;/p&gt;
&lt;h2&gt;3. 内容拓展&lt;/h2&gt;
&lt;p&gt;Logistic 回归本质上是一个 &lt;strong&gt;线性分类器&lt;/strong&gt; ，其决策边界是线性的（即 $\mathbf{w}^{\rm T} \mathbf{x} = 0$ 是一个超平面）。对于 &lt;strong&gt;线性不可分&lt;/strong&gt; 的数据，我们可以通过 &lt;strong&gt;特征工程&lt;/strong&gt; 来提升模型的表达能力。&lt;/p&gt;
&lt;p&gt;常见的特征扩展（Feature Expansion）方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多项式特征&lt;/strong&gt;：引入 $x_1^2$ 、$x_2^2$ 、$x_1x_2$ 等高阶项使决策边界变为二次曲线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式特征&lt;/strong&gt;：构造特征之间的乘积、比值等，刻画变量间的耦合关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本质上，这是通过将低维的原始特征映射到高维空间，使得数据在高维空间中变得线性可分。但需要警惕的是，特征维度过高容易导致 &lt;strong&gt;过拟合（Overfitting）&lt;/strong&gt; ，通常需要配合正则化（L1/L2 Regularization）使用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;深层问题探究&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么逻辑回归在线性回归的基础上套一层激活函数就可以进行分类呢？&lt;/p&gt;
&lt;p&gt;这个问题可以从 &lt;strong&gt;直观理解&lt;/strong&gt; 和 &lt;strong&gt;数学本质&lt;/strong&gt; 两个层面来回答：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直观理解：数值区间的映射&lt;/strong&gt;
线性回归的预测输出 $z = \mathbf{w}^{\rm T} \mathbf{x}$ 的范围是 $(-\infty, +\infty)$ ，而二分类任务要求的概率 $P(y=1|\mathbf{x})$ 必须处于 $[0, 1]$ 之间。Sigmoid 函数 $\sigma(z)$ 的作用就是将任意实数 &lt;strong&gt;映射（压缩）&lt;/strong&gt; 到 $(0, 1)$ 区间，使其具有概率的物理意义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数学本质：对数几率的线性假设&lt;/strong&gt;
逻辑回归本质上是 &lt;strong&gt;广义线性模型&lt;/strong&gt; 的一种。我们并非随意选择了一个激活函数，而是基于一个核心假设：&lt;strong&gt;样本为正类的对数几率与输入特征之间存在线性关系&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;几率定义为正类概率与负类概率的比值：&lt;/p&gt;
&lt;p&gt;$$
\frac{P(y=1|\mathbf{x})}{P(y=0|\mathbf{x})}
$$&lt;/p&gt;
&lt;p&gt;对几率取对数，即得到 &lt;strong&gt;Logit 变换&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\text{logit}(P) = \ln \frac{P(y=1 | \mathbf{x})}{P(y=0 | \mathbf{x})} = \mathbf{w}^{\rm T} \mathbf{x}
$$&lt;/p&gt;
&lt;p&gt;如果我们对上述公式进行 &lt;strong&gt;逆变换&lt;/strong&gt; ，求解 $P(y=1 | \mathbf{x})$ 就会自然导出 Sigmoid 函数的形式：&lt;/p&gt;
&lt;p&gt;$$
\frac{P}{1-P} = e^{\mathbf{w}^{\rm T} \mathbf{x}} \implies P(y=1 | \mathbf{x}) = \frac{1}{1 + e^{- \mathbf{w}^{\rm T} \mathbf{x}}} = \sigma(\mathbf{w}^{\rm T} \mathbf{x})
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：Sigmoid 函数并非仅仅是套在外部的一层壳，它是 &lt;strong&gt;对数几率线性假设&lt;/strong&gt; 在概率空间上的 &lt;strong&gt;逆映射&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;参考文献列表&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_50744311/article/details/131523136&quot;&gt;Logistic回归（逻辑回归）原理详解&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://scikit-learn.cn/stable/modules/generated/sklearn.linear_model.LogisticRegression.html&quot;&gt;Scikit-Learn 官方文档: LogisticRegression&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【博客指南】从零开始发布一篇文章</title><link>https://xingguang641.com/posts/blog/blog-guide/create-blog/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/create-blog/</guid><description>基于 Fuwari 主题的博客文章创建与部署全流程指南</description><pubDate>Wed, 22 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本文将介绍如何在 Fuwari 模板中创建新文章，并将其部署到 Nginx 服务器的完整流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;创建文章&lt;/h2&gt;
&lt;h3&gt;命令创建&lt;/h3&gt;
&lt;p&gt;你可以通过项目内置的脚本快速创建一篇文章的基础模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm new-post &quot;文章的文件名&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手动创建&lt;/h3&gt;
&lt;p&gt;你也可以直接在 &lt;code&gt;src/content/posts/&lt;/code&gt; 目录下新建 &lt;code&gt;.md&lt;/code&gt; 或 &lt;code&gt;.mdx&lt;/code&gt; 文件。请确保文件开头包含正确的 Frontmatter 信息（标题、日期、标签等元数据），以便系统正确识别文章。&lt;/p&gt;
&lt;h2&gt;构建与部署&lt;/h2&gt;
&lt;p&gt;在发布之前，需要将 Markdown 文件编译为静态 HTML 页面。&lt;/p&gt;
&lt;h3&gt;构建本地项目&lt;/h3&gt;
&lt;p&gt;在本地执行以下命令完成静态资源的构建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建结束后，项目根目录会生成一个 &lt;code&gt;dist&lt;/code&gt; 文件夹，其中包含可直接部署的站点内容。&lt;/p&gt;
&lt;h3&gt;上传至服务器&lt;/h3&gt;
&lt;p&gt;将 &lt;code&gt;dist&lt;/code&gt; 目录内容上传到服务器指定的站点路径。以下是示例命令，请根据实际情况替换 &lt;code&gt;&amp;lt;占位符&amp;gt;&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# scp -r &amp;lt;本地构建目录&amp;gt;/* &amp;lt;用户名&amp;gt;@&amp;lt;服务器IP&amp;gt;:&amp;lt;服务器站点目录&amp;gt;
scp -r ./dist/* user@192.168.1.1:/var/www/html/blog
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置权限&lt;/h2&gt;
&lt;p&gt;如果你使用 Nginx，由于上传文件的所有者通常与 Nginx 用户不同，可能会导致访问出现 &lt;code&gt;403 Forbidden&lt;/code&gt; 。因此，需要调整目录的权限。&lt;/p&gt;
&lt;p&gt;首先登录服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh user@192.168.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着依次执行以下命令，使 Nginx 能够正常读取站点内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将目录所有权交给 Web 用户 (通常是 www-data)
sudo chown -R www-data:www-data /var/www/html/blog

# 将所有文件夹的权限设置为 755 (所有者读写执行，其他人读取执行)
sudo find /var/www/html/blog -type d -exec chmod 755 {} \;

# 将所有文件的权限设置为 644 (所有者读写，其他人只读)
sudo find /var/www/html/blog -type f -exec chmod 644 {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;strong&gt;小贴士&lt;/strong&gt;：如果遇到 &lt;code&gt;403&lt;/code&gt; 或资源加载失败的问题，通常重新运行上述权限命令即可解决。
:::&lt;/p&gt;
</content:encoded></item><item><title>【开源项目部署教程】NewAPI项目教程</title><link>https://xingguang641.com/posts/github/github-project/new-api/</link><guid isPermaLink="true">https://xingguang641.com/posts/github/github-project/new-api/</guid><description>基于 Docker Compose 的 NewAPI 独立部署教程，实现数据与宿主机环境的完全隔离</description><pubDate>Wed, 22 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;NewAPI项目部署&lt;/h1&gt;
&lt;p&gt;NewAPI 是一个由 One API 深度二次开发而来的 &lt;strong&gt;统一大模型 API 管理与分发系统&lt;/strong&gt; ，它在功能上更完善、在渠道兼容性上也更灵活。通过它，你可以在同一套界面中管理不同来源的模型接口，实现统一鉴权、流量分发、负载均衡以及多渠道容灾，使调用大模型变得更加简单可靠。本文将带你了解如何通过 &lt;strong&gt;Docker Compose&lt;/strong&gt; 来快速部署一个环境隔离、配置独立、且易于维护的 NewAPI 实例，适合个人使用，也适合团队在服务器上搭建生产环境。&lt;/p&gt;
&lt;h2&gt;获取项目代码&lt;/h2&gt;
&lt;p&gt;项目开源地址如下：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;QuantumNous/new-api&quot;}&lt;/p&gt;
&lt;p&gt;首先使用 Git 将项目克隆到本地：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/QuantumNous/new-api.git
cd new-api
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置Docker&lt;/h2&gt;
&lt;p&gt;为了实现数据与宿主机的解耦，我们将使用 Docker 命名卷（Named Volumes）来替代传统的文件路径映射。请编辑项目根目录下的 &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件，将其内容替换为以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# New-API Docker Compose Configuration (独立部署版)
#
# Quick Start:
#   1. docker compose up -d
#   2. Access at http://localhost:3000
#
# Notes:
#   - 不会在宿主机创建任何文件夹（完全独立运行）
#   - 数据与日志都存储在 Docker 的内部卷中
#   - 若需查看日志: docker logs new-api
#   - 若需备份数据或日志: docker cp new-api:/app/logs ./logs_backup

version: &apos;3.4&apos;

services:
  new-api:
    image: calciumion/new-api:latest
    container_name: new-api
    restart: always
    command: --log-dir /app/logs
    ports:
      - &quot;3000:3000&quot;
    environment:
      - SQL_DSN=postgresql://root:123456@postgres:5432/new-api
#      - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Uncomment if using MySQL
      - REDIS_CONN_STRING=redis://redis
      - TZ=Asia/Shanghai
      - ERROR_LOG_ENABLED=true
      - BATCH_UPDATE_ENABLED=true
#      - STREAMING_TIMEOUT=300
#      - SESSION_SECRET=random_string
#      - SYNC_FREQUENCY=60
#      - GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
#      - UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
#      - UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js
    depends_on:
      - redis
      - postgres
#      - mysql
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;wget -q -O - http://localhost:3000/api/status | grep -o &apos;\&quot;success\&quot;:\\s*true&apos; || exit 1&quot;]
      interval: 30s
      timeout: 10s
      retries: 3
    volumes:
      - app_data:/data
      - app_logs:/app/logs

  redis:
    image: redis:latest
    container_name: redis
    restart: always

  postgres:
    image: postgres:15
    container_name: postgres
    restart: always
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: 123456
      POSTGRES_DB: new-api
    volumes:
      - pg_data:/var/lib/postgresql/data
#    ports:
#      - &quot;5432:5432&quot;

#  mysql:
#    image: mysql:8.2
#    container_name: mysql
#    restart: always
#    environment:
#      MYSQL_ROOT_PASSWORD: 123456
#      MYSQL_DATABASE: new-api
#    volumes:
#      - mysql_data:/var/lib/mysql
#    ports:
#      - &quot;3306:3306&quot;

volumes:
  pg_data:
  app_data:
  app_logs:
#  mysql_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;strong&gt;配置说明&lt;/strong&gt;：
上述配置使用了 &lt;code&gt;volumes&lt;/code&gt; 模块定义了 &lt;code&gt;pg_data&lt;/code&gt;、&lt;code&gt;app_data&lt;/code&gt; 等命名卷。这意味着数据库文件和日志只会托管在 Docker 内部，&lt;strong&gt;不会污染你的本地项目目录&lt;/strong&gt; 。即使删除了当前的文件夹，只要不手动删除 Docker Volume，数据依然保留在 Docker 中。
:::&lt;/p&gt;
&lt;h2&gt;启动远程服务&lt;/h2&gt;
&lt;p&gt;确保你的服务器已安装 Docker 和 Docker Compose。如果尚未安装，可以参考 &lt;a href=&quot;https://docs.docker.com/compose/install/&quot;&gt;官方文档&lt;/a&gt; 进行安装。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;docker-compose.yml&lt;/code&gt; 所在目录下，执行以下命令启动服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动成功后，你可以通过浏览器访问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;默认地址&lt;/strong&gt;：&lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认账号&lt;/strong&gt;：&lt;code&gt;root&lt;/code&gt; &lt;strong&gt;默认密码&lt;/strong&gt;：&lt;code&gt;123456&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常用维护指令&lt;/h2&gt;
&lt;p&gt;由于我们采用了全容器化部署，以下是一些常用的维护指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看运行日志&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs -f new-api
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;备份日志文件到本地&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker cp new-api:/app/logs ./logs_backup
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，你已经拥有了一个完全独立的 API 管理中心，无需再为繁杂的 API Key 管理而发愁了。&lt;/p&gt;
</content:encoded></item><item><title>【开源项目部署教程】JupyTrans项目教程</title><link>https://xingguang641.com/posts/github/github-project/jupyter-translate/</link><guid isPermaLink="true">https://xingguang641.com/posts/github/github-project/jupyter-translate/</guid><description>基于 LLM 和阿里云机器翻译的 Jupyter Notebook 翻译工具部署指南</description><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JupyTrans项目部署&lt;/h1&gt;
&lt;p&gt;Jupyter Translate 是一个能够将 &lt;code&gt;.ipynb&lt;/code&gt; 文件进行自动翻译的工具，支持多种翻译引擎，能够在保留代码与输出不变的情况下，批量处理 Markdown 文本与注释的语言转换。通过简单的配置，你就可以让它在笔记本中实现一键翻译，适用于整理学习资料、生成双语 Notebook 或快速阅读外文教程。下面将介绍如何安装、配置并实际使用这个工具。&lt;/p&gt;
&lt;h2&gt;获取项目代码&lt;/h2&gt;
&lt;p&gt;首先访问项目的 GitHub 仓库：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;jexonn/jupyter-translate&quot;}&lt;/p&gt;
&lt;p&gt;使用 Git 命令克隆项目到本地：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/jexonn/jupyter-translate.git
cd jupyter-translate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;p&gt;项目依赖列表在 &lt;code&gt;requirements.txt&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;strong&gt;网络提示&lt;/strong&gt;：
如果在安装过程中遇到网络超时问题，建议使用国内镜像源（如清华源）进行安装。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果仍然安装失败，你可以打开 &lt;code&gt;requirements.txt&lt;/code&gt; 查看具体内容，依次手动安装缺失的库。&lt;/p&gt;
&lt;h2&gt;配置翻译服务&lt;/h2&gt;
&lt;p&gt;该项目支持多种翻译后端。为了获得最佳的翻译质量和速度，我们通常结合使用 &lt;strong&gt;大语言模型（LLM）&lt;/strong&gt; 和 &lt;strong&gt;阿里云机器翻译&lt;/strong&gt;。你需要手动修改项目中的配置文件（通常在 &lt;code&gt;main.py&lt;/code&gt; 或独立的 &lt;code&gt;config.py&lt;/code&gt; 中）。&lt;/p&gt;
&lt;h3&gt;配置 LLM（以 DeepSeek 为例）&lt;/h3&gt;
&lt;p&gt;你可以使用任意兼容 OpenAI 接口的大模型。这里以性价比极高的 DeepSeek 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 填入你的 LLM API 信息
api_key = &quot;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;
base_url = &quot;https://api.deepseek.com/v1/&quot;
model_name = &quot;deepseek-chat&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置阿里云机器翻译（Aliyun MT）&lt;/h3&gt;
&lt;p&gt;为了处理某些特定的翻译任务，你需要配置阿里云的 AccessKey。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;注册账号&lt;/strong&gt;：前往 &lt;a href=&quot;https://www.aliyun.com/&quot;&gt;阿里云官网&lt;/a&gt; 注册并登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开通服务&lt;/strong&gt;：访问 &lt;a href=&quot;https://mt.console.aliyun.com/&quot;&gt;机器翻译控制台&lt;/a&gt; 开通服务（通常有免费额度）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取密钥&lt;/strong&gt;：访问 &lt;a href=&quot;https://ram.console.aliyun.com/manage/ak&quot;&gt;RAM 访问控制&lt;/a&gt; 创建 AccessKey。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;获取上述信息后，将其填入配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 填入阿里云 AccessKey
access_key_id = &quot;LTAIxxxxxxxxxxxxxxxx&quot;
access_key_secret = &quot;xxxxxxxxxxxxxxxxxxxxxxxx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;执行翻译任务&lt;/h2&gt;
&lt;p&gt;配置完成后，即可使用命令行工具进行翻译。&lt;/p&gt;
&lt;h3&gt;基础用法&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;-e ai&lt;/code&gt; 参数指定使用 AI 引擎进行翻译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# python main.py -e [引擎类型] [目标文件路径]
python main.py -e ai &quot;jupyter file/rag_from_scratch_1_to_4.ipynb&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;翻译结果&lt;/h3&gt;
&lt;p&gt;程序运行完成后，会在原文件同级目录下生成一个新的文件，文件名通常以 &lt;code&gt;_zh&lt;/code&gt; 结尾：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文件&lt;/strong&gt;：&lt;code&gt;rag_from_scratch_1_to_4.ipynb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;翻译后&lt;/strong&gt;：&lt;code&gt;rag_from_scratch_1_to_4_zh.ipynb&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以直接使用 Jupyter Lab 或 VS Code 打开该文件查看双语或翻译后的内容。&lt;/p&gt;
</content:encoded></item><item><title>【博客指南】Expressive-Code示例</title><link>https://xingguang641.com/posts/blog/blog-guide/expressive-code/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/expressive-code/</guid><description>演示如何在 Markdown 中使用丰富代码功能</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写在前面：本文将展示基于 &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt; 构建的增强型代码块显示效果。以下示例涵盖了从基础高亮到高级交互的各类场景，更多详细参数配置可参考官方文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心功能演示&lt;/h2&gt;
&lt;h3&gt;1. 语法高亮&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;📚 官方文档：语法高亮&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;常规语法高亮&lt;/h4&gt;
&lt;p&gt;支持主流编程语言的自动着色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;This code is syntax highlighted!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;渲染 ANSI 转义序列&lt;/h4&gt;
&lt;p&gt;可以直接渲染终端输出中的 ANSI 颜色代码，非常适合展示 CLI 工具的输出结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 终端样式&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;📚 官方文档：窗口框架&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;代码编辑器样式&lt;/h4&gt;
&lt;p&gt;模拟 IDE 窗口，支持显示文件名或完整路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Title attribute example&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;File name comment example&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;终端窗口样式&lt;/h4&gt;
&lt;p&gt;模拟命令行终端外观。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;This terminal frame has no title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;This one has a title!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;自定义窗口类型&lt;/h4&gt;
&lt;p&gt;你可以强制指定使用某种外框，或者完全移除外框。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Look ma, no frame! (无边框模式)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 强制使用代码编辑器样式，而非默认的终端样式
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 文本标记&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;📚 官方文档：文本标记&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;标记整行与多行&lt;/h4&gt;
&lt;p&gt;通过行号或范围（如 &lt;code&gt;7-8&lt;/code&gt;）来高亮特定代码行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Line 1 - 通过行号 {1} 选中
// Line 2
// Line 3
// Line 4 - 通过行号 {4} 选中
// Line 5
// Line 6
// Line 7 - 通过范围 {7-8} 选中
// Line 8 - 通过范围 {7-8} 选中
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指定标记类型（高亮、新增、删除）&lt;/h4&gt;
&lt;p&gt;除了默认的高亮，还支持 &lt;code&gt;ins&lt;/code&gt;（新增/绿色）和 &lt;code&gt;del&lt;/code&gt;（删除/红色）样式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;这一行被标记为删除 (del)&apos;)
  // 下面两行被标记为新增 (ins)
  console.log(&apos;this is the second inserted line&apos;)

  return &apos;这一行使用默认的中性标记 (mark)&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;带标签的行标记&lt;/h4&gt;
&lt;p&gt;可以在高亮行的右侧添加文本标签，用于解释代码逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;长标签文本布局&lt;/h4&gt;
&lt;p&gt;当标签文本较长时，会自动调整布局以保持美观。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Diff 语法支持&lt;/h4&gt;
&lt;p&gt;直接支持标准的 diff 格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+这一行将被标记为新增
-这一行将被标记为删除
这是一个普通行
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;混合使用 Diff 与语法高亮&lt;/h4&gt;
&lt;p&gt;你可以在保留 JavaScript 等语言高亮的同时，使用 diff 标记。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // 整个代码块将被高亮显示为 JavaScript
    // 同时我们仍然可以使用 diff 符号
-   console.log(&apos;Old code to be removed&apos;)
+   console.log(&apos;New and shiny code!&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;行内文本高亮&lt;/h4&gt;
&lt;p&gt;不标记整行，而是通过字符串匹配高亮行内的特定文本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // Mark any given text inside lines
  return &apos;Multiple matches of the given text are supported&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;正则表达式匹配&lt;/h4&gt;
&lt;p&gt;支持使用正则进行灵活的文本匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;The words yes and yep will be marked.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;转义处理&lt;/h4&gt;
&lt;p&gt;在正则模式中匹配正斜杠。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;自定义行内标记样式&lt;/h4&gt;
&lt;p&gt;行内文本同样支持 &lt;code&gt;ins&lt;/code&gt; 和 &lt;code&gt;del&lt;/code&gt; 样式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;These are inserted and deleted marker types&apos;);
  // return 语句使用默认的标记类型
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 自动换行&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;📚 官方文档：自动换行&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;开启与关闭&lt;/h4&gt;
&lt;p&gt;控制长代码行是否自动换行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 开启自动换行 (wrap)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 关闭自动换行 (wrap=false)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;智能缩进保留&lt;/h4&gt;
&lt;p&gt;开启换行时，是否保留第二行的缩进对齐。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 开启缩进保留 (默认行为)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 关闭缩进保留 (文字将顶格换行)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;插件功能演示&lt;/h2&gt;
&lt;h3&gt;1. 代码折叠&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;📚 官方文档：代码折叠&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;支持将不重要的样板代码（Boilerplate）折叠隐藏，点击即可展开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这部分样板代码默认会被折叠
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// 这部分代码默认可见
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // 这里也可以设置折叠区域
  const a = 1
  const b = 2
  const c = a + b

  // 这部分保持可见
  console.log(`Calculation result: ${a} + ${b} = ${c}`)
  return c
}

// 结尾的代码再次折叠
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: &apos;End of example boilerplate code&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 代码行号&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;📚 官方文档：行号显示&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;控制行号显示&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 显式开启行号
console.log(&apos;Greetings from line 2!&apos;)
console.log(&apos;I am on line 3&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 显式禁用行号
console.log(&apos;Hello?&apos;)
console.log(&apos;Sorry, do you know what line I am on?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;自定义起始行号&lt;/h4&gt;
&lt;p&gt;在展示代码片段（而非完整文件）时非常有用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Greetings from line 5!&apos;)
console.log(&apos;I am on line 6&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【博客指南】Markdown扩展功能</title><link>https://xingguang641.com/posts/blog/blog-guide/markdown-extended/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/markdown-extended/</guid><description>在 Fuwari 中了解更多关于 Markdown 功能的信息</description><pubDate>Wed, 10 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub仓库卡片&lt;/h2&gt;
&lt;p&gt;你可以添加链接到 GitHub 仓库的动态卡片；页面加载时会从 GitHub API 拉取该仓库的信息。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Fabrizz/MMM-OnSpotify&quot;}&lt;/p&gt;
&lt;p&gt;创建一个包含代码的 GitHub 仓库卡片： &lt;code&gt;::github{repo=&quot;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&quot;}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;saicaca/fuwari&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义多彩提示框&lt;/h2&gt;
&lt;p&gt;支持以下类型的提示框（admonitions）： &lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
强调用户即使在快速浏览时也应该注意的信息。
:::&lt;/p&gt;
&lt;p&gt;:::tip
可选信息，用于帮助用户更好地完成任务。
:::&lt;/p&gt;
&lt;p&gt;:::important
用户成功所必需的重要信息。
:::&lt;/p&gt;
&lt;p&gt;:::warning
由于潜在风险，需要用户立即关注的关键信息。
:::&lt;/p&gt;
&lt;p&gt;:::caution
某个操作可能带来的负面后果。
:::&lt;/p&gt;
&lt;h3&gt;基本语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
强调用户即使在快速浏览时也应注意的信息。
:::

:::tip
可选信息，用于帮助用户更顺利地完成任务。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义标题&lt;/h3&gt;
&lt;p&gt;提示框的标题可以自定义。&lt;/p&gt;
&lt;p&gt;:::note[MY CUSTOM TITLE]
这是一个带有自定义标题的备注。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[MY CUSTOM TITLE]
这是一个带有自定义标题的备注。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub 语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
同样支持 &lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;GitHub 语法&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; 同样支持 GitHub 的语法。

&amp;gt; [!TIP]
&amp;gt; 同样支持 GitHub 的语法。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;隐藏内容&lt;/h3&gt;
&lt;p&gt;你可以在文本中隐藏内容。文本同样支持 &lt;strong&gt;Markdown&lt;/strong&gt; 语法。&lt;/p&gt;
&lt;p&gt;The content :spoiler[is hidden &lt;strong&gt;ayyy&lt;/strong&gt;]!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The content :spoiler[is hidden **ayyy**]!

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【博客指南】Fuwari简易指南</title><link>https://xingguang641.com/posts/blog/blog-guide/index/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/index/</guid><description>如何使用这个博客模板</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;封面图片来源: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;来源&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个博客模板是使用 &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; 构建的。对于本指南中未提及的内容，你可以在 &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt; 中找到答案。&lt;/p&gt;
&lt;h2&gt;文章的前置信息&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的标题。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的发布日期。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的简短描述，在首页显示。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的封面图片路径。&amp;lt;br/&amp;gt;1. 以 &lt;code&gt;http://&lt;/code&gt; 或 &lt;code&gt;https://&lt;/code&gt; 开头：使用网络图片&amp;lt;br/&amp;gt;2. 以 &lt;code&gt;/&lt;/code&gt; 开头：对应 &lt;code&gt;public&lt;/code&gt; 目录中的图片&amp;lt;br/&amp;gt;3. 没有以上前缀：相对于该 Markdown 文件的路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的标签。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章的分类。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果这篇文章仍为草稿，则不会显示。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;文件的存放位置&lt;/h2&gt;
&lt;p&gt;你的文章文件应放在 &lt;code&gt;src/content/posts/&lt;/code&gt; 目录下。你也可以创建子目录，以便更好地整理文章和资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【博客指南】Markdown示例</title><link>https://xingguang641.com/posts/blog/blog-guide/markdown/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/markdown/</guid><description>一个简单的 Markdown 博客文章示例</description><pubDate>Sun, 01 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;An h1 header&lt;/h1&gt;
&lt;p&gt;Paragraphs are separated by a blank line.&lt;/p&gt;
&lt;p&gt;2nd paragraph. &lt;em&gt;Italic&lt;/em&gt;, &lt;strong&gt;bold&lt;/strong&gt;, and &lt;code&gt;monospace&lt;/code&gt;. Itemized lists
look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this one&lt;/li&gt;
&lt;li&gt;that one&lt;/li&gt;
&lt;li&gt;the other one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Block quotes are
written like so.&lt;/p&gt;
&lt;p&gt;They can span multiple paragraphs,
if you like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., &quot;it&apos;s all
in chapters 12--14&quot;). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺&lt;/p&gt;
&lt;h2&gt;An h2 header&lt;/h2&gt;
&lt;p&gt;Here&apos;s a numbered list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;first item&lt;/li&gt;
&lt;li&gt;second item&lt;/li&gt;
&lt;li&gt;third item&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here&apos;s a code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define foobar() {
    print &quot;Welcome to flavor country!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(which makes copying &amp;amp; pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
# Quick, count to ten!
for i in range(10):
    # (but not *too* quick)
    time.sleep(0.5)
    print i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An h3 header&lt;/h3&gt;
&lt;p&gt;Now a nested list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, get these ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;carrots&lt;/li&gt;
&lt;li&gt;celery&lt;/li&gt;
&lt;li&gt;lentils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boil some water.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dump everything in the pot and follow
this algorithm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; find wooden spoon
 uncover pot
 stir
 cover pot
 balance wooden spoon precariously on pot handle
 wait 10 minutes
 goto first step (or shut off burner when done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not bump wooden spoon or it will fall.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).&lt;/p&gt;
&lt;p&gt;Here&apos;s a link to &lt;a href=&quot;http://foo.bar&quot;&gt;a website&lt;/a&gt;, to a &lt;a href=&quot;local-doc.html&quot;&gt;local
doc&lt;/a&gt;, and to a &lt;a href=&quot;#an-h2-header&quot;&gt;section heading in the current
doc&lt;/a&gt;. Here&apos;s a footnote [^1].&lt;/p&gt;
&lt;p&gt;[^1]: Footnote text goes here.&lt;/p&gt;
&lt;p&gt;Tables can look like this:&lt;/p&gt;
&lt;p&gt;size material color&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;9 leather brown
10 hemp canvas natural
11 glass transparent&lt;/p&gt;
&lt;p&gt;Table: Shoes, their sizes, and what they&apos;re made of&lt;/p&gt;
&lt;p&gt;(The above is the caption for the table.) Pandoc also supports
multi-line tables:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;keyword text&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;red Sunsets, apples, and
other red or reddish
things.&lt;/p&gt;
&lt;p&gt;green Leaves, grass, frogs
and other things it&apos;s
not easy being.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A horizontal rule follows.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s a definition list:&lt;/p&gt;
&lt;p&gt;apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There&apos;s no &quot;e&quot; in tomatoe.&lt;/p&gt;
&lt;p&gt;Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a &quot;line block&quot;:&lt;/p&gt;
&lt;p&gt;| Line one
| Line too
| Line tree&lt;/p&gt;
&lt;p&gt;and images can be specified like so:&lt;/p&gt;
&lt;p&gt;Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:&lt;/p&gt;
&lt;p&gt;$$I = \int \rho R^{2} dV$$&lt;/p&gt;
&lt;p&gt;$$
\begin{equation*}
\pi
=3.1415926535
;8979323846;2643383279;5028841971;6939937510;5820974944
;5923078164;0628620899;8628034825;3421170679;\ldots
\end{equation*}
$$&lt;/p&gt;
&lt;p&gt;And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: `foo`, *bar*, etc.&lt;/p&gt;
</content:encoded></item><item><title>【博客指南】在博客中嵌入视频</title><link>https://xingguang641.com/posts/blog/blog-guide/video/</link><guid isPermaLink="true">https://xingguang641.com/posts/blog/blog-guide/video/</guid><description>这篇文章演示了如何在博客文章中嵌入视频</description><pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;只需从 YouTube 或其他平台复制嵌入代码，然后将其粘贴到 Markdown 文件中即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: 在博客中嵌入视频
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>