<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>TonyYin&apos;s Blog</title><description>Yuwendong Yin&apos;s personal blog</description><link>https://www.tonyyin0418.com</link><item><title>无锡随记</title><link>https://www.tonyyin0418.com/blog/life/260726_wuxi</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/life/260726_wuxi</guid><description>7月24日至26日在无锡的旅行记录。</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇 Life 文章需要在博客中输入密码后阅读。&lt;/p&gt;</content:encoded></item><item><title>上海随记</title><link>https://www.tonyyin0418.com/blog/life/260719_shanghai</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/life/260719_shanghai</guid><description>7月17日至19日在上海的旅行记录。</description><pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇 Life 文章需要在博客中输入密码后阅读。&lt;/p&gt;</content:encoded></item><item><title>并行处理器：从客户端到云端</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap7</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap7</guid><description>武汉大学2025秋季学期 计算机组成 第七章</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;p&gt;Chapter 7：Parallel Processors from Client to Cloud&lt;/p&gt;
&lt;p&gt;由于功耗墙的限制，单核处理器的性能提升遇到瓶颈，所以转向多核以追求更高能效。&lt;/p&gt;
&lt;p&gt;Parallelism 分为：&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;本章重点：MIMD，向量机、GPU。&lt;/p&gt;
&lt;h2&gt;Parallel Programming&lt;/h2&gt;
&lt;p&gt;要通过并行计算得到较好性能，难点在：&lt;strong&gt;负载均衡、协调、通信开销&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Amdahl&apos;s Law&lt;/h3&gt;
&lt;p&gt;由于串行部分的存在，加速比是有上限的。&lt;/p&gt;
&lt;p&gt;阿姆达尔定律，用来计算 &lt;strong&gt;加速比&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果有 100 个处理器，想要达到加速比为 90 倍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$$
\text{Speedup}=\frac{T_{\text{old}}}{T_{\text{new}}}=\frac{T_{\text{old}}}{T_{\text{old}}-T_{\text{parallelizable}}+\dfrac{T_{\text{parallelizable}}}{100}}=90
$$&lt;/p&gt;
&lt;p&gt;用 $F$ 表示可并行指令的比例，则：
$$
\text{Speedup}=\frac{1}{(1-F_{\text{parallelizable}})+\dfrac{F_{\text{parallelizable}}}{100}}
$$
解得需要 $F_{\text{parallelizable}}=0.999$ 才能达到。&lt;/p&gt;
&lt;h3&gt;Scaling&lt;/h3&gt;
&lt;p&gt;在 HPC 中，&lt;strong&gt;Scaling&lt;/strong&gt; 概念，是用来衡量 &lt;strong&gt;系统或算法扩展能力&lt;/strong&gt; 的核心指标。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strong scaling&lt;/strong&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;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak scaling&lt;/strong&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;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vector Processors&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/img-36.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;MIMD 是现在最常用的，多指令多数据流。&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;向量寄存器 (v0-v31)&lt;/strong&gt;：每个寄存器可以存放 32 个 64 位元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;向量指令&lt;/strong&gt;：每次可以算 32 个数。常用的有：&lt;/p&gt;
&lt;ul&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;GPU Architectures&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;进程与线程：一个进程可以包含若干个线程。所有线程共用一个虚拟内存空间，但每个线程有自己独立的寄存器和PC。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>存储系统</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap6</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap6</guid><description>武汉大学2025秋季学期 计算机组成 第五章</description><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;p&gt;Chapter 6&lt;/p&gt;
&lt;p&gt;本章目标：&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;</content:encoded></item><item><title>存储系统</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap5</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap5</guid><description>武汉大学2025秋季学期 计算机组成 第五章</description><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;p&gt;Chapter 5: Large and Fast: Exploiting Memory Hierarchy&lt;/p&gt;
&lt;p&gt;本章目标：&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;存储容量的数量级：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-27.png&quot; alt=&quot;存储容量的数量级&quot;&gt;&lt;/p&gt;
&lt;p&gt;**存储系统的层次：**从 CPU 开始，&lt;strong&gt;由近到远，速度由快到慢，容量由小到大，价格越来越低&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-28.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Principle of Locality&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述存储结构可以根据这两者，让存储系统更快。&lt;/p&gt;
&lt;h3&gt;Memory Technology&lt;/h3&gt;
&lt;p&gt;常用存储技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static RAM, &lt;strong&gt;SRAM&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用于 Cache，最快。&lt;/li&gt;
&lt;li&gt;由&lt;strong&gt;六晶体管&lt;/strong&gt;构成，数据一直保存在稳定电路状态中，断电即丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dynamic RAM, &lt;strong&gt;DRAM&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用于 Memory，比 Cache 慢但便宜。&lt;/li&gt;
&lt;li&gt;每个 bit 由电容 + 晶体管组成，用电荷表示0/1&lt;/li&gt;
&lt;li&gt;需要&lt;strong&gt;周期性 Refresh&lt;/strong&gt; 电荷，避免状态消失，断电即丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flash Memory&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;li&gt;&lt;strong&gt;Magnetic disk&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;h2&gt;Cache Basics&lt;/h2&gt;
&lt;p&gt;Cache 是 Memory 部分内容的映像，通过 Cache 可以快速访问 Memory 中的数据，从而提高系统性能。&lt;/p&gt;
&lt;p&gt;Memory 和 Cache 之间以**块（Block）**为单位进行数据传输。&lt;/p&gt;
&lt;p&gt;下面假设在 32 位机器上，一个块的大小是 4 bytes，一个 byte 有 8 位，地址共 32 位长。但实际考试中，可能会考察其他自定义情况。&lt;/p&gt;
&lt;h3&gt;Memory 地址结构&lt;/h3&gt;
&lt;p&gt;**Memory 地址结构：**从高位到低位划分为 &lt;code&gt;| Tag | Index | Offset |&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tag&lt;/strong&gt;：标识该块来自 Memory 的哪个地址范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index&lt;/strong&gt;：标识该块在 Cache 中的位置，&lt;strong&gt;Tag + Index 是精确到块的地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offset&lt;/strong&gt;：精确到字节的地址，标识该块内的具体字节位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Direct Mapped Cache 映射方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Direct Mapped Cache&lt;/strong&gt;：把 Memory &lt;strong&gt;块号（Tag + Index）&lt;/strong&gt;，通过&lt;strong&gt;取模运算&lt;/strong&gt;得到 Index，根据 Index 决定其映射到 Cache 中的哪个块。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-38.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Cache 块结构&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cache 块结构：&lt;/strong&gt; &lt;code&gt;| Valid | Tag | Data |&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Valid&lt;/strong&gt;：标识该块是否包含有效数据。valid = 0 代表该块为空，valid = 1 代表该块有有效数据。清零 Cache 时只需要写 0 即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag&lt;/strong&gt;：存储 Memory 地址的 Tag 部分，这样标记该块在 Memory 中的位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;：存储从 Memory 读取的数据块。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cache 访问流程&lt;/h3&gt;
&lt;p&gt;当 CPU 访问 Memory 地址时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 Memory 的 &lt;strong&gt;Index&lt;/strong&gt; 字段送到译码器，译码器定位到 Cache 的对应块。&lt;/li&gt;
&lt;li&gt;比较 Memory 地址的 &lt;strong&gt;Tag&lt;/strong&gt; 与 Cache 块中存储的 Tag。&lt;/li&gt;
&lt;li&gt;如果 Tag 匹配且 Valid = 1，则&lt;strong&gt;命中（Hit）&lt;/strong&gt;，使用 &lt;strong&gt;Offset&lt;/strong&gt; 从 Data 中提取数据。根据 Offset 决定返回哪个字节。&lt;/li&gt;
&lt;li&gt;如果 Tag 不匹配或 Valid = 0，则&lt;strong&gt;未命中（Miss）&lt;/strong&gt;，需要从 Memory 加载数据到 Cache。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;images/img-39.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Block Size&lt;/h3&gt;
&lt;p&gt;Block Size 越大，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命中率越高，更多数据更快访问。&lt;/li&gt;
&lt;li&gt;同一个块会被不同任务频繁调度，更频繁的改内容，引起污染问题。&lt;/li&gt;
&lt;li&gt;需要更长的传输时间，因为要传输更多的数据。
&lt;ul&gt;
&lt;li&gt;这个问题可以用 critical word first 策略解决，先传输关键字，再传输其他数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此需要根据 CPU 属性，选择合适的 Block Size。&lt;/p&gt;
&lt;h2&gt;Cache Write Policies 写策略&lt;/h2&gt;
&lt;h3&gt;Write-Through&lt;/h3&gt;
&lt;p&gt;若 Cache Hit，则同时写入 Memory 和 Cache。&lt;/p&gt;
&lt;p&gt;但是这个方法很慢。&lt;/p&gt;
&lt;p&gt;一般需要 &lt;strong&gt;Write buffer&lt;/strong&gt; 优化一下，CPU 写到 buffer 之后就可以继续干别的了，buffer 慢慢写到 memory 里面。&lt;/p&gt;
&lt;h3&gt;Write-Back&lt;/h3&gt;
&lt;p&gt;若 Cache Hit，只写入 Cache，不写入 Memory。&lt;/p&gt;
&lt;p&gt;增加硬件支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给 Cache 增加一位 &lt;code&gt;Dirty&lt;/code&gt; 位，若 Dirty = 1，则表示 Cache 块内容被修改，与 Memory 不同步。&lt;/li&gt;
&lt;li&gt;当 Cache 块即将被新内容替换时，若 Dirty = 1，则写入 Memory。&lt;/li&gt;
&lt;li&gt;MSI 协议？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Write Allocation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;若 Cache Miss&lt;/strong&gt;，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 Write-Through 策略，都可以。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于 Write-Back 策略：先把内容读到 Cache 中，然后写 Cache 的内容并标记 Dirty.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cache Performance&lt;/h2&gt;
&lt;p&gt;Cache 命中率越高，Pipeline 阻塞越少，性能越好。下面讨论如何评价 Cache 命中率。&lt;/p&gt;
&lt;p&gt;需要先计算 Memory Stall Cycles，然后计算 CPI。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\text{Memory Stall Cycles}
&amp;#x26;= \text{Cache Misses} \times \text{Miss Penalty} \
&amp;#x26;= \text{Instruction Count} \times \text{Miss Rate} \times \text{Miss Penalty} \
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Memory Stall Cycles 是 Cache Miss 的惩罚总周期数。&lt;/li&gt;
&lt;li&gt;Instruction Count 是程序中指令的总数&lt;/li&gt;
&lt;li&gt;Miss Rate 是每条指令平均 Cache Miss 的概率，计算方法是 Misses / Instruction&lt;/li&gt;
&lt;li&gt;Miss Penalty 是 Cache Miss 的惩罚周期数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CPI 计算&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;这部分常用，并结合后面的内容一起考。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于通常用 CPI 来评价 CPU 性能，这里也用 CPI 来评价 Cache 性能。
$$
\text{CPI} = \text{Base CPI} + \frac{\text{Memory Stall Cycles}}{\text{Instruction Count}}
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base CPI 的含义是：&lt;strong&gt;假设所有访存都不发生 stall（即理想 Cache / 理想内存）的情况&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Average memory access time, AMAT&lt;/h3&gt;
&lt;p&gt;Average memory access time (AMAT) 是 Cache 的平均访问时间。通常以 CPU 周期为计量单位。&lt;/p&gt;
&lt;p&gt;$$
\text{AMAT} = \text{Hit Time} + \text{Miss Rate} \times \text{Miss Penalty}
$$&lt;/p&gt;
&lt;p&gt;因为任何一次访问，最开始都要从 Cache 读一下试试，所以 Hit Time 概率为 $100%$。&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hit Time 是 Cache Hit 的访问时间。&lt;/li&gt;
&lt;li&gt;Miss Rate 是 Cache Miss 的概率。&lt;/li&gt;
&lt;li&gt;Miss Penalty 是 Cache Miss 的惩罚时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Performance Summary&lt;/h3&gt;
&lt;p&gt;综上所述，要提高性能，只能通过提高 Cache 命中率，具体方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;增加块大小 Block Size；&lt;/li&gt;
&lt;li&gt;增加相联度 Associativity、替换算法；&lt;/li&gt;
&lt;li&gt;增加多级 Cache。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体方法会在下文中介绍。&lt;/p&gt;
&lt;h2&gt;Associative Caches 相联缓存与替换&lt;/h2&gt;
&lt;h3&gt;Associative Caches 相联缓存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fully associative 全相联&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;允许 Memory 中的任意块，都可以映射到 Cache 中的任意一个块。&lt;/li&gt;
&lt;li&gt;但需要存储全部的 Tag + Index，成本高。&lt;/li&gt;
&lt;li&gt;每个条目都需要设计一个比较电路，成本高，功耗高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;n-way associative 组相联&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;对 Cache Block 分组，&lt;strong&gt;n-way 代表一组里有 n 个块&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;核心策略：&lt;strong&gt;组内使用全相联，组间使用直接映射&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Memory 先&lt;strong&gt;对组数取模&lt;/strong&gt;得到 Index，然后根据 Index 找到对应的组；组内任意存放，使用全相联的方式查找。&lt;/li&gt;
&lt;li&gt;一个组有多少块，就需要多少个比较电路。
&lt;ul&gt;
&lt;li&gt;相对于全相联，减少了比较电路的数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;images/img-40.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相联度&lt;/strong&gt;：组相联的相联度为 n，全相联的相联度为 Cache Block count，直接映射的相联度为 1。&lt;/p&gt;
&lt;p&gt;相联度越高，命中率越高。但相联度过高后，组合电路过于复杂，提升效果有限，会导致成本功耗提高。&lt;/p&gt;
&lt;p&gt;因此需要根据实际情况选择合适的相联度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-29.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，是一个 4-相联度的 Cache。&lt;/p&gt;
&lt;p&gt;先通过 Address 的 Index 定位到某一组，即一行的 4 个块，之后再在组内根据 Tag 查找。&lt;/p&gt;
&lt;h3&gt;Replacement Policies 替换策略&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;在组相连 Cache 中，优先写入 non-valid 的块。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然而，当一个组满了，需要再存一个块时，需要选择一个块进行替换。有如下策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Least-recently used, LRU&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;选择&lt;strong&gt;最近最少使用的块&lt;/strong&gt;进行替换。&lt;/li&gt;
&lt;li&gt;对于 2-way 或者 4-way 还可以用电路实现，太大的时候就很难实现。&lt;/li&gt;
&lt;li&gt;严格实现需要时间戳，时间成本很高。&lt;/li&gt;
&lt;li&gt;因此实际情况下，会用一些其他方法近似实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;随机选择一个块进行替换，一些情况下表现并不差。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Multilevel Caches&lt;/h2&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Primary Cache：集成在 CPU 中，速度最快，容量最小。&lt;/li&gt;
&lt;li&gt;Level-2 Cache：集成在主板，也有在 CPU 中的，更慢，更大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;访问&lt;/h3&gt;
&lt;p&gt;先访问 L1，若未命中，则访问 L2，若未命中，则访问 Memory。访问到了之后，将数据写回 L1 和 L2。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-30.png&quot; alt=&quot;Multilevel Caches&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图是对访问延迟的分解表示，真正访问 Memory 的概率为 L1 Miss Rate * L2 Miss Rate。&lt;/p&gt;
&lt;p&gt;衡量性能时，还是需要&lt;strong&gt;计算 CPI&lt;/strong&gt;：
$$
\begin{aligned}
\text{CPI} &amp;#x26;= \text{Base CPI} \
&amp;#x26;+ \text{L1 Miss Rate} \times \text{L1 Miss Penalty} \
&amp;#x26;+ \text{L1 Miss Rate} \times \text{L2 Miss Rate} \times \text{L2 Miss Penalty} \
\end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;循环分块&lt;/h3&gt;
&lt;p&gt;不是减少计算量，而是&lt;strong&gt;减少 Cache miss，提高数据局部性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从最经典的例子开始，一个 C 语言下的矩阵乘法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (i = 0; i &amp;#x3C; N; i++)
  for (j = 0; j &amp;#x3C; N; j++)
    for (k = 0; k &amp;#x3C; N; k++)
      C[i][j] += A[i][k] * B[k][j];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 C 语言中数据是按行连续存储的，$B_{k, j}$ 连续访问的元素在内存中不连续，产生大量 cache miss，数据反复从内存加载。&lt;/p&gt;
&lt;p&gt;我们需要 &lt;strong&gt;把“大循环”拆成“能装进 Cache 的小块”，让数据在 Cache 里被反复用完再换掉。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Virtual Memory&lt;/h2&gt;
&lt;h3&gt;Virtual Memory Basics&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Virtual Memory&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Memory + Disk + OS&lt;/li&gt;
&lt;li&gt;是一种机制，它让主存（DRAM）充当了磁盘（Disk）的缓存。&lt;/li&gt;
&lt;li&gt;把 Disk 提供给 CPU 一个虚拟的 Memory 地址空间，让 CPU 可以像访问 Memory 一样访问 Disk.&lt;/li&gt;
&lt;li&gt;每个程序拥有私有的虚拟地址空间，虚拟空间一定要映射到物理空间。&lt;/li&gt;
&lt;li&gt;Disk 和 Memory 之间以 &lt;strong&gt;Page&lt;/strong&gt; 为单位，交换数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Memory Layout&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-31.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面讲解如何实现上述功能。&lt;/p&gt;
&lt;h3&gt;Address Translation 地址转换&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/img-32.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;左图是虚拟存储和物理存储的关系，以&lt;strong&gt;页&lt;/strong&gt;为存储的基本单位（一个页是一个固定的大小）。其中有两种地址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟地址 Virtual Address&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理地址 Physical Address&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;地址较低位是 &lt;strong&gt;Page Offset 页内地址&lt;/strong&gt;；较高位是 &lt;strong&gt;Page Number 页号&lt;/strong&gt;，因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VA 的结构为：&lt;strong&gt;| VPN | Offset |&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;PA 的结构为：&lt;strong&gt;| PPN | Offset |&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们要把 Virtual Address 对应到 Physical Address，只需要 Virtual Page Number 转换为 Physical Page Number，页内偏移量 Offset 保持不变，这就是 Address Translation 过程。&lt;/p&gt;
&lt;h2&gt;Page Table 页表&lt;/h2&gt;
&lt;p&gt;上面的 Translation 过程需要维护一个 &lt;strong&gt;页表 Page Table&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图是页表的作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-41.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;基本结构&lt;/h3&gt;
&lt;p&gt;每个&lt;strong&gt;页表项 Page Table Entry 对应一个虚拟页号VPN&lt;/strong&gt;，在表中可以查询到 &lt;strong&gt;虚拟页号 VPN 对应的&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理页号 PPN&lt;/strong&gt;：物理内存中的位置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有效位 Valid Bit&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;1 表示，在主存 Memory 中，正常；&lt;/li&gt;
&lt;li&gt;0 表示，不在主存 Memory 中，此时 PTE 里存的通常是 Disk Address。&lt;/li&gt;
&lt;li&gt;为 0 时，必定触发 &lt;strong&gt;Page Fault&lt;/strong&gt;，进入 OS 中对应的异常处理程序，处理后，会把 Disk 内容读取到 Memory 中，之后PTE 存的就是 Memory Address了，详见后文。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;页表存储在哪？多进程？&lt;/p&gt;
&lt;p&gt;每个进程都有一个自己的页表。&lt;/p&gt;
&lt;p&gt;正在运行的进程，其&lt;strong&gt;页表存储在 CPU 中的页表寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CPU 中有一组特殊寄存器，只有内核态（正在运行的进程）可以访问，称为 控制状态寄存器 CSR。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下图是页表的运行过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-41.png&quot; alt=&quot;&quot;&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;界限寄存器 Limit Register&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用一个寄存器来限制页表的大小，只为进程实际使用的部分分配页表。&lt;/li&gt;
&lt;li&gt;因为大多数进程实际上只使用了虚拟地址空间的一小部分，页表只需要覆盖进程实际使用的地址范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反向页表 Inverted Page Table&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;按物理页号索引，而不是虚拟页号，只需要一张表；&lt;/li&gt;
&lt;li&gt;大幅减小表的大小，但没法快速查找，遍历比较慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级页表 Multiple levels of page tables&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;li&gt;极大地减少了内存占用，也不需要连续的大块内存，是目前最主流的实现方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Page Fault&lt;/h3&gt;
&lt;p&gt;当 Memory 中没有，只在 Disk 中有数据的时候，OS 需要花费大量时间，把缺失的页&lt;strong&gt;从 Disk 调往 Memory&lt;/strong&gt;。然后更新页表的 PPN 为 Memory 中的一个物理地址，更新 Valid Bit 为 0.&lt;/p&gt;
&lt;p&gt;因此需要尽可能减小 Page Fault Rate.&lt;/p&gt;
&lt;h3&gt;Replacement and Writes&lt;/h3&gt;
&lt;p&gt;这部分和 Cache 很类似。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Replacement&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Memory 满了的时候需要替换一些来自 Disk 的页。&lt;/li&gt;
&lt;li&gt;可以采用 LRU 算法，和 Cache 中的实现基本一样。在上述 Page Table 的基础上，加一个 &lt;strong&gt;Reference Bit&lt;/strong&gt;，表示 &lt;strong&gt;页面最近是否被访问过&lt;/strong&gt;。被访问时，该位置1，操作系统定期清零。替换时选择一个为 0 的页来替换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Writes&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写 Disk 非常慢，且必须按页写入，不能只写一个字节。因此 Write-through 的效率是不能接受的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只能采用 Write-back 策略&lt;/strong&gt;。只更新 Memory 中的 Page，只有当 Page 被替换时，才将其写回 Disk.&lt;/li&gt;
&lt;li&gt;硬件上，和 Cache 一样，需要在 PTE 中加一个 &lt;strong&gt;Dirty bit&lt;/strong&gt;，标记该内容是否需要写回磁盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Translation Look-aside Buffer, TLB&lt;/h2&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;TLB 简称 &lt;strong&gt;快表&lt;/strong&gt;，是 CPU 内部的一个&lt;strong&gt;专用高速缓存&lt;/strong&gt;，专门用来存储&lt;strong&gt;最近使用的页表项&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只在 CPU 中有一个，因此 TLB 只存储正在运行的进程，切换进程时清空。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TLB 的命中率非常高，99%以上。&lt;/p&gt;
&lt;p&gt;TLB 可以理解为页表的 Cache，页表的部分内容映射到 TLB。&lt;/p&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;p&gt;和 Cache 非常类似。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内部组成&lt;/strong&gt;：TLB 的每一行通常包含以下部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tag&lt;/strong&gt;：虚拟页号 (Virtual Page Number) 的一部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;：物理页号 (Physical Page Address)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态位&lt;/strong&gt;：Valid, Dirty, Ref.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 TLB 很小，为了降低缺失率，通常采用&lt;strong&gt;全相联&lt;/strong&gt;结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-33.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;TLB Misses&lt;/h3&gt;
&lt;p&gt;有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A：页面在主存中，只是 TLB 没记录。
&lt;ul&gt;
&lt;li&gt;从内存中的页表加载 PTE 到 TLB，然后重试。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B：Page Fault，页面不在主存中
&lt;ul&gt;
&lt;li&gt;触发缺页异常，由操作系统 (OS) 处理，从磁盘读取页面。&lt;/li&gt;
&lt;li&gt;不归 TLB 处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Interaction with Cache&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;访问流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU 发出虚拟地址，给 TLB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB&lt;/strong&gt; 将虚拟地址转换为物理地址，若缺失就访问页表。&lt;/li&gt;
&lt;li&gt;使用物理地址访问 &lt;strong&gt;Cache&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;images/img-34.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;images/img-35.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Memory Protection&lt;/h2&gt;
&lt;p&gt;多个进程可以共享内存，但必须防止它们乱访问彼此的数据。&lt;/p&gt;
&lt;p&gt;用户态 / 内核态权限管理。&lt;/p&gt;
&lt;h2&gt;Dependability&lt;/h2&gt;
&lt;p&gt;内存可能出错，需要很好的校验、纠错机制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-37.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面是 1bit 的校验和纠错算法。&lt;/p&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$ 开始。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验位&lt;/strong&gt;：选取 $2^0, 2^1, 2^2\cdots$ 为校验位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验位 $p_{2^k}$ 的范围&lt;/strong&gt;：&lt;strong&gt;编号的二进制表示中，第 $k$ 位为 $1$ 的所有 bit。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Encoding&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;偶校验&lt;/strong&gt;：给校验位 $0$ 或者 $1$，&lt;strong&gt;使校验位覆盖的所有 bit 中，有偶数个 1&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Decoding&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;找错误&lt;/strong&gt;：根据目前值，重新计算所有校验位。若 $p=1$，说明该校验位错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纠错&lt;/strong&gt;：假设 $p_1=1, p_2=0, p_4=1,p_8=0$，可知编号第 $1,3$ 位为 $1$ 的 bit 出错，也就是第 5 位出错，对应 $d_2$。纠错只需要将其取反即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果需要 2bits 的校验，注意到，上述算法可能不起效，因为两个错就可以让偶校验失效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2bits 校验&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入全局校验位 $p_n$，对 &lt;strong&gt;整个码字&lt;/strong&gt; 做偶校验。&lt;/li&gt;
&lt;li&gt;若前面的校验位对 1bits 校验提示有错误，但 $p_n$ 没错，则说明是 2bits 错误。&lt;/li&gt;
&lt;li&gt;同样兼容 1bits 错误纠错：当 $p_n$ 显示错误，1bits 也显示错误，则是此类情况。&lt;/li&gt;
&lt;li&gt;并不能具体判断出是哪 2bits，不具备 2bits 纠错功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The BIG Picture&lt;/h2&gt;
&lt;p&gt;Cache、虚拟内存等其实都遵循同一套思想，都有四个核心层级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Block placement&lt;/strong&gt;：数据怎么存？
&lt;ul&gt;
&lt;li&gt;Direct mapped&lt;/li&gt;
&lt;li&gt;n-way set associative&lt;/li&gt;
&lt;li&gt;Fully associative&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h2&gt;&lt;strong&gt;Finding a block&lt;/strong&gt;：数据怎么检索？&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replacement&lt;/strong&gt;：存满了，换谁？
&lt;ul&gt;
&lt;li&gt;LRU&lt;/li&gt;
&lt;li&gt;Random&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write policy&lt;/strong&gt;：怎么写数据？
&lt;ul&gt;
&lt;li&gt;Write-Through 写直达&lt;/li&gt;
&lt;li&gt;Write-Back 写回&lt;/li&gt;
&lt;li&gt;VM 只能接受 Write-Back，因为磁盘写入太慢了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>处理器的设计</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap4</guid><description>武汉大学2025秋季学期 计算机组成 第四章</description><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;[toc]&lt;/p&gt;
&lt;p&gt;Chapter 4: The Processor&lt;/p&gt;
&lt;p&gt;本章目标：用硬件描述语言设计一个 Single-Core Processor。&lt;/p&gt;
&lt;p&gt;本章将实现支持七条指令的处理器设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Memory：lw, sw.&lt;/li&gt;
&lt;li&gt;Arithmetic：add, sub, and, or.&lt;/li&gt;
&lt;li&gt;Conditional branch：beq.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本章涉及单周期处理器和流水线处理器的设计。&lt;/p&gt;
&lt;p&gt;CPU 有两部分组成：DataPath 数据通路和 ControlPath 控制通路。&lt;/p&gt;
&lt;h2&gt;Instruction Execution&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;取指，&lt;strong&gt;同时进行&lt;/strong&gt;两步：
&lt;ul&gt;
&lt;li&gt;把 PC 的值送给指令存储器，指令存储器从内存取出指令。&lt;/li&gt;
&lt;li&gt;$\text{PC}\leftarrow \text{PC}+4$.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;li&gt;执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Components&lt;/h2&gt;
&lt;p&gt;功能部件分为：组合逻辑部件和时序部件（状态部件）。&lt;/p&gt;
&lt;p&gt;用低电平表示 0，用高电平表示 1。一根导线只可以传输一个 0 或 1。&lt;/p&gt;
&lt;h3&gt;组合逻辑部件&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/img-04.png&quot; alt=&quot;image-20251014183635159&quot;&gt;&lt;/p&gt;
&lt;h3&gt;时序部件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Edge-triggered&lt;/strong&gt;：每个周期有两个边缘，只有在上升沿或下降沿时，才把输入的数据送到输出端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Register&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在回路中存储数据。&lt;/li&gt;
&lt;li&gt;有一个信号 $\text{Write}$，表示是否把输入端的数据送到输出端：
&lt;ul&gt;
&lt;li&gt;$\overline{\text{Write}}$ 或者 $\text{Write}^{#}$ 表示低电平有效，其余情况为高电平有效。&lt;/li&gt;
&lt;li&gt;常见表示为 $R/\overline W$，含义为高电平读，低电平写。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Clocking Methodology&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;这个过程发生在时钟周期之间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;DataPath&lt;/h2&gt;
&lt;h3&gt;Fetch Instruction&lt;/h3&gt;
&lt;p&gt;同时进行两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 PC 的值送给指令存储器，指令存储器从内存取出指令。&lt;/li&gt;
&lt;li&gt;$\text{PC}\leftarrow \text{PC}+4$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要的部件：adder，寄存器 PC，Instruction memory。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-05.png&quot; alt=&quot;image-20251014190151964&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于这些部件都在单周期内完成，所以可以同时进行两个操作。&lt;/p&gt;
&lt;h3&gt;R-format Instructions&lt;/h3&gt;
&lt;p&gt;R-format：&lt;code&gt;add x5, x6, x7&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要的部件&lt;/strong&gt;：寄存器堆 RF，ALU。&lt;/p&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;——操作数给 RF，取出两个寄存器的值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算&lt;/strong&gt;——送到 ALU 进行计算；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写寄存器&lt;/strong&gt;——操作数给 RF，指定 RF 的 Write 信号为高电平，把 ALU 的输出送回到 RF 中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示意图&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-06.png&quot; alt=&quot;image-20251014191334357&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Load Instructions&lt;/h3&gt;
&lt;p&gt;Load：&lt;code&gt;lw x5, 3(x6)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要的部件&lt;/strong&gt;：Imm Generator，ALU，Memory，寄存器堆 RF。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Imm Generator&lt;/strong&gt;：把立即数部分取出，进行符号扩展，以匹配 32 位。&lt;/p&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;ul&gt;
&lt;li&gt;从 RF 中&lt;strong&gt;取出寄存器的值&lt;/strong&gt;（并行）；&lt;/li&gt;
&lt;li&gt;立即数通过 Imm Gen 进行 &lt;strong&gt;sign-extend&lt;/strong&gt; 后送到 ALU（并行）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读 Memory&lt;/strong&gt;：把刚才计算得到地址给 Memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取值&lt;/strong&gt;：从 Memory 读，调整 Memory 的 Read 信号为高电平，得到的值送到 RF 中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示意图&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-07.png&quot; alt=&quot;image-20251014192844388&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Store Instruction&lt;/h3&gt;
&lt;p&gt;Store：&lt;code&gt;sw x5, 3(x6)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt;：与 Load 类似，计算地址部分完全相同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算地址&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;从 RF 中&lt;strong&gt;取出寄存器的值&lt;/strong&gt;（并行）；&lt;/li&gt;
&lt;li&gt;立即数通过 Imm Gen 进行 &lt;strong&gt;sign-extend&lt;/strong&gt; 后送到 ALU（并行）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写 Memory&lt;/strong&gt;：把刚才计算得到地址给 Memory，调整 Memory 的 Write 信号为高电平，把 RF 中的值送到 Memory。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Branch Instruction&lt;/h3&gt;
&lt;p&gt;Branch：&lt;code&gt;beq x5, x6, label&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要的部件&lt;/strong&gt;：Imm Generator，ALU，寄存器堆 RF，PC，额外的 adder。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读 RF&lt;/strong&gt;：从 RF 中取出两个寄存器的值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算比较&lt;/strong&gt;（并行）：用 ALU 进行减法运算，判断是否相等；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算分支地址&lt;/strong&gt;（并行）：
&lt;ul&gt;
&lt;li&gt;立即数通过 Imm Gen 进行 &lt;strong&gt;sign-extend&lt;/strong&gt;，之后&lt;strong&gt;左移一位&lt;/strong&gt;，算出 PC + Imm；&lt;/li&gt;
&lt;li&gt;注意是 PC + Imm，不是 (PC + 4) + Imm，即当前正在运行的指令的地址，把这个本周期最开始的值送到 adder，详见最后的 Full Datapath 图。&lt;/li&gt;
&lt;li&gt;注意这需要额外的 adder，不能与 PC = PC + 4 的 adder 共用，因为这个分支有两种情况，要提前算出来 PC+4 和 PC+Imm 两种情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写 PC&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果 ALU 的 Zero 信号为高电平，表示相等，把 PC + Imm 送到 PC；&lt;/li&gt;
&lt;li&gt;否则，把 PC + 4 送到 PC。&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;p&gt;&lt;img src=&quot;images/img-08.png&quot; alt=&quot;image-20251014194659706&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Full DataPath&lt;/h3&gt;
&lt;p&gt;综上所述，可能同一个输入，需要支持多种来源的信号，因此还需要补充 &lt;strong&gt;MUX 多路选择器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如 ALU 的第二个输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可能是立即数 Imm：Load / Store 指令计算地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;也可能是寄存器的值： Branch 指令。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示意图&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-09.png&quot; alt=&quot;image-20251014195257170&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Control&lt;/h2&gt;
&lt;p&gt;图中蓝色的信号是控制信号。&lt;/p&gt;
&lt;p&gt;其中 &lt;strong&gt;PCSrc&lt;/strong&gt; 控制信号比较特殊：只有当前指令是 Branch 并且 ALU 的 Zero=1 时，才把 PC + Imm 送到 PC，否则都送 PC + 4。&lt;/p&gt;
&lt;p&gt;上述的 Branch 信号需要依赖 Control Path 生成。&lt;/p&gt;
&lt;p&gt;其余&lt;strong&gt;控制信号由 Control Path 生成&lt;/strong&gt;，下面具体说明。&lt;/p&gt;
&lt;h3&gt;ALU Control&lt;/h3&gt;
&lt;p&gt;ALU 需要负责多种指令，对应不同的控制信号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;R-format 指令：&lt;code&gt;add, sub, and, or&lt;/code&gt;，需要 4 种控制信号。&lt;/li&gt;
&lt;li&gt;Load / Store： &lt;code&gt;add&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Branch： &lt;code&gt;sub&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Control Path 根据指令的 op, func3, func7 字段生成 ALU 的控制信号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-10.png&quot; alt=&quot;image-20251014200714659&quot;&gt;&lt;/p&gt;
&lt;p&gt;真值表允许出现：$0, 1, \text{X}$ 三种状态，其中 $\text{X}$ 表示“无关（don&apos;t care）”；高阻态通常记为 $\text{Z}$。&lt;/p&gt;
&lt;h3&gt;Control Unit&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/img-11.png&quot; alt=&quot;image-20251016100651314&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 ALU Control 之外，Control Path 还需要根据 op 部分，生成以下控制信号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Branch：是否为 beq 指令。&lt;/li&gt;
&lt;li&gt;MemRead：是否读内存。&lt;/li&gt;
&lt;li&gt;MemtoReg： 内存读出的值是否写回寄存器。&lt;/li&gt;
&lt;li&gt;ALUop：ALU Control 所需的操作码。&lt;/li&gt;
&lt;li&gt;MemWrite：是否写内存。&lt;/li&gt;
&lt;li&gt;ALUSrc：选择 ALU 第二个输入。&lt;/li&gt;
&lt;li&gt;RegWrite：是否写寄存器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Control Unit Implementation&lt;/h3&gt;
&lt;p&gt;有两种控制器实现方式。&lt;/p&gt;
&lt;p&gt;一般情况下，简单指令用组合逻辑实现，复杂指令用存储器实现。&lt;/p&gt;
&lt;h4&gt;Combinational Logic Control&lt;/h4&gt;
&lt;p&gt;组合逻辑控制器。&lt;/p&gt;
&lt;p&gt;根据上述逻辑，列出真值表，用组合逻辑实现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-12.png&quot; alt=&quot;image-20251016103236835&quot;&gt;&lt;/p&gt;
&lt;p&gt;输入 Inst[6:0]，经过组合逻辑，输出各个控制信号。&lt;/p&gt;
&lt;h4&gt;ROM-based Control&lt;/h4&gt;
&lt;p&gt;存储型控制器 / 微程序控制器。&lt;/p&gt;
&lt;p&gt;对于 RISC-V 这种精简指令集，组合逻辑实现足够快，复杂性可控制。而对于 x86 等复杂指令集，组合逻辑实现会非常复杂，不太稳定。&lt;/p&gt;
&lt;p&gt;为了简化设计，可以用 Read-Only Memory, ROM 来实现 Control Path。&lt;/p&gt;
&lt;p&gt;ROM 中，每一行是一个存储单元，对应一个&lt;strong&gt;微指令&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址是指令的 op 部分；&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;p&gt;使用时，用指令的 op 部分，作为地址，从 ROM 中取出对应的控制信号。&lt;/p&gt;
&lt;h2&gt;补充指令&lt;/h2&gt;
&lt;h3&gt;JAL Instruction&lt;/h3&gt;
&lt;p&gt;JAL：&lt;code&gt;jal rd, Imm&lt;/code&gt;。&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;ul&gt;
&lt;li&gt;把立即数通过 Imm Gen 符号扩展，之后左移一位，之后计算 PC + Imm；&lt;/li&gt;
&lt;li&gt;计算 PC + 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;把 PC + Imm 写回 PC，把 PC + 4 写回寄存器 rd。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Data / Control Flow&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为了把 &lt;code&gt;PC + Imm&lt;/code&gt; 送到 PC，需要对右上角的 Mux 增加 &lt;code&gt;PCSrc&lt;/code&gt; 信号（与分支控制组合），在需要跳转时选择 &lt;code&gt;PC + Imm&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了把 &lt;code&gt;PC + 4&lt;/code&gt; 写回寄存器 &lt;code&gt;rd&lt;/code&gt;（常用作链接寄存器 x1），需要在 WB 端扩展写回选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;MemtoReg&lt;/code&gt; 扩展为两位的选择信号，例如 &lt;code&gt;00: ALUOut, 01: ReadData, 10: PC+4&lt;/code&gt;（也可命名为 &lt;code&gt;ResultSrc/WBSel&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RegWrite=1&lt;/code&gt; 时写回 &lt;code&gt;rd&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JALR Instruction&lt;/h3&gt;
&lt;p&gt;JALR：&lt;code&gt;jalr rd, rs1, Imm&lt;/code&gt;&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;ul&gt;
&lt;li&gt;取寄存器 rs1，把立即数通过 Imm Gen 符号扩展，计算 rs1 + Imm&lt;/li&gt;
&lt;li&gt;计算 PC + 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;把 rs1 + Imm 写回 PC，把 PC + 4 写回寄存器 rd。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设计图和真值表如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-13.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-14.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;ORI Instruction&lt;/h3&gt;
&lt;p&gt;I-type：&lt;code&gt;ori rd, rs1, imm&lt;/code&gt;。对 &lt;code&gt;rs1&lt;/code&gt; 与有符号扩展后的 &lt;code&gt;imm&lt;/code&gt; 做按位或，结果写回 &lt;code&gt;rd&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;控制要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RegWrite = 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALUSrc = 1&lt;/code&gt;（ALU 第二个输入选立即数）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALUCtrl = OR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MemRead = 0&lt;/code&gt;, &lt;code&gt;MemWrite = 0&lt;/code&gt;, &lt;code&gt;MemtoReg = 0&lt;/code&gt;（WB 选 ALU 结果）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;code&gt;ori x5, x3, 0x0FF&lt;/code&gt; 将 &lt;code&gt;x3&lt;/code&gt; 与 &lt;code&gt;0x0FF&lt;/code&gt; 做 OR，结果写入 &lt;code&gt;x5&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;p&gt;为了衡量性能，需要计算&lt;strong&gt;各指令的延迟路径与总延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CPU 时钟周期由&lt;strong&gt;最长的延迟路径&lt;/strong&gt;决定，关键路径决定：更短的指令也需要等待这个最长指令完成后，才能进入下一个周期。&lt;/p&gt;
&lt;p&gt;计算指令的延迟路径，常常忽略 MUX, Control Unit, PC 等延迟较小的部件。之后根据并行和串行的关系，计算各个指令的延迟路径。&lt;/p&gt;
&lt;p&gt;后序会介绍&lt;strong&gt;流水线&lt;/strong&gt;，可以提升性能。&lt;/p&gt;
&lt;h2&gt;Multicycle&lt;/h2&gt;
&lt;p&gt;多周期处理器：&lt;strong&gt;每条指令分多个时钟周期完成&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;把每条指令分成多个 step，每个 step 在一个时钟周期内完成。这样可以增加主频，CPI 增大。&lt;/p&gt;
&lt;p&gt;这样可以让较短的指令执行更快，因为不需要等待最长指令完成，但还是串行执行每条指令。&lt;/p&gt;
&lt;h3&gt;Datapath&lt;/h3&gt;
&lt;p&gt;由于每个时钟周期结束后，指令可能并没有结束，因此每个 unit 的输出值需要&lt;strong&gt;存储在寄存器&lt;/strong&gt;中，供下一个周期继续执行这条指令的后续 step 使用，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IR：Instruction Register，存储当前指令。&lt;/li&gt;
&lt;li&gt;A, B：存储从寄存器堆读出的两个寄存器&lt;/li&gt;
&lt;li&gt;MDR：Memory Data Register，存储从内存读出的数据&lt;/li&gt;
&lt;li&gt;ALUOut：存储 ALU 的输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，由于每个周期只执行一个 step，因此原来有很多部件是多余的，可以只保留一份。比如 Instruction Memory 和 Data Memory 可以合并成一个 Memory；Adder 和 ALU 也可以合并成一个 ALU。&lt;/p&gt;
&lt;p&gt;简略的 Datapath 示意图如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-15.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Control&lt;/h3&gt;
&lt;p&gt;当然多周期也需要 Control Path 来生成控制信号，所需要的控制信号类别不变，而每个指令的每个 step 都需要不同的控制信号。&lt;/p&gt;
&lt;p&gt;因此需要引入&lt;strong&gt;时序信号&lt;/strong&gt;，表示当前处于哪个 step，由&lt;strong&gt;状态机&lt;/strong&gt;生成对应的控制信号。&lt;/p&gt;
&lt;p&gt;状态机是一个&lt;strong&gt;有限状态机&lt;/strong&gt;，每个状态对应一个 step。输入操作码和时钟信号，输出当前 step 所需的控制信号。&lt;/p&gt;
&lt;h2&gt;Pipeline&lt;/h2&gt;
&lt;p&gt;在 Multicycle 的基础上，Pipeline 技术能让 &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;images/img-16.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，对于中间的周期，每个周期都能完成一条指令的执行。&lt;/p&gt;
&lt;h3&gt;RISC-V Pipeline&lt;/h3&gt;
&lt;p&gt;RISC-V 指令被拆分成五个阶段，每个阶段对应一个时钟周期：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;IF：Instruction Fetch，取指令&lt;/li&gt;
&lt;li&gt;ID：Instruction Decode，译码 &amp;#x26; 读寄存器&lt;/li&gt;
&lt;li&gt;EX：Instruction Execute，执行操作 或 计算地址&lt;/li&gt;
&lt;li&gt;MEM：Memory Access，访存&lt;/li&gt;
&lt;li&gt;WB：Write Back，写回寄存器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这被称为 5-stage Pipeline 五级流水线。如果 $5$ 个阶段延迟相等，则：&lt;/p&gt;
&lt;p&gt;$$
\text{Execution Time}&lt;em&gt;{\text{pipelined}}=\frac{\text{Execution Time}&lt;/em&gt;{\text{non-pipeline}}}{5}
$$&lt;/p&gt;
&lt;p&gt;然而需要注意，&lt;strong&gt;单个指令的 Latency 并没有减少&lt;/strong&gt;，而且与单周期对比，由于必须经过五级，单个指令的延迟反而增加了。&lt;/p&gt;
&lt;p&gt;之所以能加速，主要是因为 &lt;strong&gt;Throughput 吞吐量&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;流水线的实现与 Multicycle 类似，也需要在每个阶段之间增加&lt;strong&gt;Pipeline registers 流水线寄存器&lt;/strong&gt;，其命名比较统一：IF/ID、ID/EX、EX/MEM、MEM/WB，分别存储每个阶段的输出，供下一个阶段使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-17.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Pipeline Datapath&lt;/h3&gt;
&lt;p&gt;然而，现有五级流水线图还缺少“写回路径与目的寄存器 rd 的逐级保留”，导致 WB 阶段无法把结果写回寄存器堆。&lt;/p&gt;
&lt;p&gt;WB 需要写回目的寄存器号 &lt;code&gt;rd&lt;/code&gt; 和写回数据，所以前者需要 &lt;strong&gt;逐级保留&lt;/strong&gt; 于每个 Pipeline Register。&lt;/p&gt;
&lt;p&gt;这并不涉及 Structure Hazards，因为 RegisterFile 的 Read / Write 都很快，一个周期内可以同时完成 Read 和 Write。所以虽然有多个阶段争用一个硬件资源，但是并不冲突。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-18.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Datapath的分析有两种画图方法：
&lt;strong&gt;Multi-Cycle Pipeline Diagram&lt;/strong&gt;：二维图。&lt;/p&gt;
&lt;p&gt;![[img-23.png]]
&lt;strong&gt;Single-Cycle Pipeline Diagram&lt;/strong&gt;：只看一个时刻，看每个部件上，有哪些指令在执行。&lt;/p&gt;
&lt;p&gt;![[img-24.png]]&lt;/p&gt;
&lt;h3&gt;Pipeline Control&lt;/h3&gt;
&lt;p&gt;设计 Pipelined Control，需要分析&lt;strong&gt;每条指令在每个阶段需要的信号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Control 结构也需要利用寄存器，&lt;strong&gt;逐级保存&lt;/strong&gt;后续阶段需要的信号。Data 和 Control 两个寄存器合二为一。&lt;/p&gt;
&lt;p&gt;![[img-25.png]]&lt;/p&gt;
&lt;p&gt;上述流水线的并行执行会引入&lt;strong&gt;冒险 Hazards&lt;/strong&gt;，有三种类型。&lt;/p&gt;
&lt;h2&gt;Structure Hazards&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;/p&gt;
&lt;h2&gt;Data Hazards&lt;/h2&gt;
&lt;p&gt;数据相关引起 Data Hazards. &lt;strong&gt;解决方法：Forwarding / Bypassing&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;add x3, x1, x2&lt;/code&gt; 结束后，才有 &lt;code&gt;x3&lt;/code&gt; 的结果，才能执行 &lt;code&gt;sub x5, x3, x4&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;正常情况下，需要等五个周期全结束，WB 后，才能取得 x3 的数据。然而这样的话，Pipeline 完全没有并行，效率很低。这种现象叫做 &lt;strong&gt;Pipeline Bubble&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-19.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;而之所以只需要 bubble 两个周期，是因为前半个周期完成 WB，后半个周期完成 ID，这是 RF 的特性造成的：&lt;em&gt;RF 用下降沿 Write（前半个周期），用后半个周期 Read&lt;/em&gt;；其余结构都是在上升沿 Write。&lt;/p&gt;
&lt;h3&gt;Forwarding&lt;/h3&gt;
&lt;p&gt;如图所示，直接增加从 &lt;strong&gt;EX旁路&lt;/strong&gt;——EX/MEM.ALUOut -&gt; ALU.in 的数据通路，无需等待结果被 WB 到寄存器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-20.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;考虑另一个更复杂的例子，会发现还可能涉及第一条和第三条指令之间的数据相关。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-26.png&quot; alt=&quot;&quot;&gt;
这时需要增加另一种通路，叫 &lt;strong&gt;MEM 旁路&lt;/strong&gt;：MEM/WB.ALUOut -&gt; ALU.in.&lt;/p&gt;
&lt;h3&gt;ALU Instructions&lt;/h3&gt;
&lt;p&gt;我们发现，对于连续的 ALU 指令，共有两种类型旁路：&lt;strong&gt;EX 旁路&lt;/strong&gt; 和 &lt;strong&gt;MEM 旁路&lt;/strong&gt;。下面分析这两种旁路的触发条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EX Forwarding&lt;/strong&gt;：从 EX / MEM 到 ID / EX。&lt;/p&gt;
&lt;p&gt;触发条件：
$$
EX_Fwd = \mathrm{EX/MEM}.\mathrm{RegWrite} \land (\mathrm{EX/MEM}.rd \neq 0) \land (\mathrm{EX/MEM}.rd \in {\mathrm{ID/EX}.rs1,; \mathrm{ID/EX}.rs2}).
$$
含义：上一条指令的 ALU 结果已经在 EX/MEM 中，但尚未来得及写回寄存器，如果下一条指令的源操作数正好依赖该结果，就直接将 EX/MEM.ALUOut 旁路到 ALU 的输入端（控制信号 &lt;code&gt;ForwardA/ForwardB = 10&lt;/code&gt;），避免等待 WB。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MEM Forwarding&lt;/strong&gt;：从 MEM / WB 到 ID / EX。&lt;/p&gt;
&lt;p&gt;触发条件：
$$
\begin{aligned}
MEM_Fwd =
\mathrm{MEM/WB}.\mathrm{RegWrite} \
\land (\mathrm{MEM/WB}.rd \neq 0)\
\land (\mathrm{MEM/WB}.rd \in {\mathrm{ID/EX}.rs1,; \mathrm{ID/EX}.rs2})\
\land \neg EX_Hit.
\end{aligned}
$$
其中 $EX_Hit$ 表示对应源寄存器已经满足 EX forwarding 的条件。含义：当结果已经进入 MEM/WB，但因为顺序问题还没写回寄存器文件时，通过旁路把 &lt;code&gt;MEM/WB.WriteData&lt;/code&gt; 送到 ALU 输入（控制信号 &lt;code&gt;ForwardA/ForwardB = 01&lt;/code&gt;）。如果同一源同时满足 EX 和 MEM forwarding，优先选择 EX forwarding，因为它携带的结果更新更早。&lt;/p&gt;
&lt;h3&gt;Load-Use Instructions&lt;/h3&gt;
&lt;p&gt;例如：&lt;code&gt;lw  x7, 0(x3)&lt;/code&gt;，&lt;code&gt;add x8, x7, x9&lt;/code&gt; 存在数据相关。虽然可以增加 &lt;strong&gt;MEM 旁路&lt;/strong&gt;，但是由于 load 指令需要用的是 MEM 的结果，Pipeline 不得不阻塞一个周期。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-21.png&quot; alt=&quot;&quot;&gt;
Stall 检测条件：
$$
LoadUseStall = \mathrm{ID/EX}.\mathrm{MemRead} \land (\mathrm{ID/EX}.rd \in {\mathrm{IF/ID}.rs1,; \mathrm{IF/ID}.rs2}).
$$
控制动作：
$$
\begin{cases}
PCWrite = \neg LoadUseStall,\
IF/ID.Write = \neg LoadUseStall,\
ID/EX.Flush = LoadUseStall.
\end{cases}
$$
也就是说，一旦侦测到 load-use hazard，就冻结取指/译码阶段，并将下一拍的 ID/EX 清零（插入 bubble），等待数据从 MEM 阶段返回。&lt;/p&gt;
&lt;h3&gt;Code Scheduling&lt;/h3&gt;
&lt;p&gt;这是另一种在某些情况有用的解决方法。编译器自动给汇编指令调整顺序，来避免 Data Hazards。如图所示，是 C 代码 &lt;code&gt;a = b + e; c = b + f;&lt;/code&gt; 的两种实现，右侧的就可以避免 Hazards。
&lt;img src=&quot;images/img-22.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Control Hazards&lt;/h2&gt;
&lt;h2&gt;Exceptions and Interrupts&lt;/h2&gt;
&lt;p&gt;软件引起异常（Exceptions）硬件引起终端（Interrupts）&lt;/p&gt;
&lt;p&gt;异常会触发内核态切换，从 User Mode 切换到 Kernel Mode。&lt;/p&gt;
&lt;p&gt;处理方法：SEPC，SCAUSE。&lt;/p&gt;
&lt;p&gt;Multiple Exceptions：&lt;/p&gt;
&lt;p&gt;乱序：CPU 为了执行速度更快，采用乱序执行。Load 指令发生冒险，必须需要阻塞的时候，会空一个周期。CPU 会自动找一个后续指令提前执行。这是 CPU 的自动操作。&lt;/p&gt;
&lt;p&gt;多发射：&lt;/p&gt;
&lt;p&gt;Imprecise exceptions：流水线上同时执行多个指令，&lt;/p&gt;
&lt;p&gt;ILP：&lt;/p&gt;</content:encoded></item><item><title>计算机的算术运算</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap3</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap3</guid><description>武汉大学2025秋季学期 计算机组成 第三章</description><pubDate>Thu, 09 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;images/img-01.png&quot; alt=&quot;img-01&quot;&gt;&lt;/p&gt;
&lt;h2&gt;运算器&lt;/h2&gt;
&lt;p&gt;主要关心整数运算器，设计 ALU (Arithmetic Logic Unit)。&lt;/p&gt;
&lt;p&gt;ALU 负责执行所有的算术和逻辑操作，比如加法、减法、按位与、按位或等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;饱和运算 Saturating operations&lt;/strong&gt;：如果结果超出范围，就取最大值或最小值。&lt;/p&gt;
&lt;h3&gt;乘法&lt;/h3&gt;
&lt;p&gt;RISC-V 的乘法有四个核心指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mul&lt;/code&gt;：返回低位结果（RV32I 为低 32 位，RV64I 为低 64 位）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mulh&lt;/code&gt;：返回有符号数乘法的高位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mulhu&lt;/code&gt;：返回无符号乘法的高位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mulhsu&lt;/code&gt;：第一个操作数有符号、第二个无符号，返回高位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;基本移位-累加算法&lt;/h4&gt;
&lt;p&gt;设被乘数为 $M$、乘数为 $Q$，都是 $n$ 位补码数。硬件通常准备一个 $2n$ 位的乘积寄存器 $P$。逐位处理乘数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化 $P \leftarrow 0$。&lt;/li&gt;
&lt;li&gt;对 $i = 0 \dots n-1$：若 $Q_i = 1$，则将 $M$ 左移 $i$ 位后加到 $P$ 上。&lt;/li&gt;
&lt;li&gt;结果 $P$ 含 $2n$ 位，低位对应 &lt;code&gt;mul&lt;/code&gt;，高位对应 &lt;code&gt;mulh&lt;/code&gt;/&lt;code&gt;mulhu&lt;/code&gt; 等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;乘法的数学形式：
$$
P = M \times Q = \sum_{i=0}^{n-1} q_i \cdot M \cdot 2^i
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：计算 $M = 13 = 01101_2$、$Q = 11 = 01011_2$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环 0：$Q_0 = 1$，部分积加 $M$ 得 $00000 + 01101 = 01101$。&lt;/li&gt;
&lt;li&gt;循环 1：$Q_1 = 1$，加 $M \ll 1 = 11010$，结果 $100111$。&lt;/li&gt;
&lt;li&gt;继续直到循环 4，最终得到 $P = 10001111_2 = 143_{10}$，符合 $13 \times 11 = 143$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Booth 算法（补码优化）&lt;/h4&gt;
&lt;p&gt;核心思想是压缩连串的 $1$ 为一次加减：观察乘数当前位 $Q_0$ 与“伪位” $Q_{-1}$。&lt;/p&gt;
&lt;p&gt;| $Q_0 Q_{-1}$ | 操作 |
| --- | --- |
| 00 或 11 | 不操作，仅进行算术右移 |
| 01 | 将 $M$ 加入部分积 |
| 10 | 将 $M$ 的补码减去（即加 $-M$） |&lt;/p&gt;
&lt;p&gt;每轮后对部分积 $A$、乘数 $Q$、伪位 $Q_{-1}$ 做算术右移，循环 $n$ 次。Booth 对乘数含连续 $1$ 的情形尤其高效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Booth 示例&lt;/strong&gt;：计算 $M = -6 = 11010_2$、$Q = 13 = 01101_2$（5 位）：&lt;/p&gt;
&lt;p&gt;| 步骤 | $Q_0 Q_{-1}$ | 操作 | $A$ | $Q$ | $Q_{-1}$ |
| --- | --- | --- | --- | --- | --- |
| 0 | 初始 | 初值 | 00000 | 01101 | 0 |
| 1 | 10 | $A = A + (-M)$ | 00110 | 01101 | 0 |
|   |  | 算术右移 | 00011 | 00110 | 1 |
| 2 | 01 | $A = A + M$ | 11101 | 00110 | 1 |
|   |  | 算术右移 | 11110 | 10011 | 0 |
| 3 | 10 | $A = A + (-M)$ | 00100 | 10011 | 0 |
|   |  | 算术右移 | 00010 | 01001 | 1 |
| 4 | 11 | 无操作 | 00010 | 01001 | 1 |
|   |  | 算术右移 | 00001 | 00100 | 1 |
| 5 | 01 | $A = A + M$ | 11011 | 00100 | 1 |
|   |  | 算术右移 | 11101 | 10010 | 0 |&lt;/p&gt;
&lt;p&gt;最终组合 $A:Q = 11101,10010_2$，解析为十进制 $-78$，与 $-6 \times 13$ 一致。&lt;/p&gt;
&lt;h4&gt;阵列乘法器与壁虎乘法器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;阵列（array）乘法器利用规则结构把所有部分积一次性生成并通过多级加法器相加，代价是硬件面积较大。&lt;/li&gt;
&lt;li&gt;Wallace tree / CSA（Carry Save Adder）乘法器在部分积累加阶段采用进位保存，加速运算；末级再用快速加法器获得最终结果。&lt;/li&gt;
&lt;li&gt;单精度浮点尾数含隐含位，共 $24$ 位；双精度为 $53$ 位 =&gt; 浮点乘法器至少需要 $24\times 24$ 或 $53\times 53$ 位部分积。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;浮点数加法&lt;/h3&gt;
&lt;p&gt;浮点加法遵循“对齐—运算—规格化—舍入—异常”流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对齐指数&lt;/strong&gt;：指数差 $d = E_A - E_B$。较小指数的尾数右移 $d$ 位并产生 GRS 位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尾数运算&lt;/strong&gt;：若符号相同则相加，符号相反则相减。暂不处理溢出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规格化&lt;/strong&gt;：若结果 $M$ 超过 $[1,2)$，则右移一位并使指数加一；若低于 $[1,2)$，则左移并指数减一，直至归一化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;舍入&lt;/strong&gt;：依据 GRS 位选择舍入模式（默认 RNE 最近偶数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理&lt;/strong&gt;：检测指数上溢（产生 $\pm\infty$）或下溢（可能转为次正规或置 0），记录异常标志。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;流水线实现通过分离上述阶段提升吞吐量。&lt;/p&gt;
&lt;h2&gt;浮点数寄存器&lt;/h2&gt;
&lt;p&gt;RISC-V 有 32 个浮点数寄存器，命名为 &lt;code&gt;f0&lt;/code&gt; 到 &lt;code&gt;f31&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每个寄存器都是 64 位，若要存储 32 位浮点数，则低 32 位存储数据，高 32 位置零。&lt;/p&gt;
&lt;h2&gt;加法与减法&lt;/h2&gt;
&lt;h3&gt;基本定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;无符号 $n$ 位整数的范围：
$$
0 \leq X_{\text{unsigned}} \leq 2^n - 1
$$&lt;/li&gt;
&lt;li&gt;补码（有符号）$n$ 位整数的范围：
$$
-2^{n-1} \leq X_{\text{two&apos;s}} \leq 2^{n-1} - 1
$$&lt;/li&gt;
&lt;li&gt;补码求负：
$$
-B = \overline{B} + 1
$$
其中 $\overline{B}$ 表示按位取反。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二进制补码复习&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;最高位（MSB）是符号位：0 表示非负，1 表示负。&lt;/li&gt;
&lt;li&gt;求负流程：写出绝对值二进制 -&gt; 按位取反 -&gt; 加 $1$。&lt;/li&gt;
&lt;li&gt;扩展位宽时使用符号扩展（sign extension），即复制符号位保持数值不变。&lt;/li&gt;
&lt;li&gt;算术右移（ASR）保持符号：右移后空出的最高位填入原符号位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;加法器结构&lt;/h3&gt;
&lt;p&gt;Ripple-carry 加法器以“半加器 + 全加器”串联实现：
$$
S_i = a_i \oplus b_i \oplus c_i, \quad c_{i+1} = (a_i \land b_i) \lor (c_i \land (a_i \oplus b_i))
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进位传播延迟&lt;/strong&gt;：$O(n)$，因此引入 CLA（Carry Lookahead）、CSA（Carry Save）等加速器件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;补码减法&lt;/strong&gt;：
$$
A - B = A + (-B)
$$
直接用加法器完成减法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Carry Lookahead (CLA) 通过预先计算“生成”与“传递”信号缩短延迟：
$$
g_i = a_i b_i, \quad p_i = a_i \oplus b_i, \quad c_{i+1} = g_i \lor (p_i c_i)
$$
展开前两级可得：
$$
c_1 = g_0 \lor (p_0 c_0), \quad c_2 = g_1 \lor (p_1 g_0) \lor (p_1 p_0 c_0)
$$
CSA（Carry Save Adder）适合多操作数累加（例如乘法部分积），将每一位的进位暂存到下一层，最终再以一次 CLA 合并。&lt;/p&gt;
&lt;h3&gt;溢出检测&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;无符号：若最终进位 $c_n = 1$ 则发生溢出。&lt;/li&gt;
&lt;li&gt;补码：
$$
\text{Overflow} = c_n \oplus c_{n-1}
$$
等价于“同号相加得到异号”或“异号相减得到异号”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;例题：$8$ 位补码加法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;计算 $0101,1100_2 (92_{10}) + 0100,0011_2 (67_{10})$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$c_7 = 1$、$c_8 = 0$，故 $c_7 \oplus c_8 = 1$ =&gt; 溢出，结果非法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算 $0101,1100_2 + 1010,0100_2$：符号位为 $0$ 与 $1$，异号相加 =&gt; 不会溢出。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;减法演练&lt;/h3&gt;
&lt;p&gt;目标：计算 $79 - 26$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写出操作数：
$$
A = 79 = 0100,1111_2, \quad B = 26 = 0001,1010_2
$$&lt;/li&gt;
&lt;li&gt;求 $-B$：
$$
\overline{B} = 1110,0101_2, \quad -B = \overline{B} + 1 = 1110,0110_2
$$&lt;/li&gt;
&lt;li&gt;执行补码加法：
$$
A + (-B) = 0100,1111_2 + 1110,0110_2 = 1,0011,0101_2
$$&lt;/li&gt;
&lt;li&gt;舍弃最高进位，得到 $0011,0101_2 = 53_{10}$。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常见陷阱&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;硬件只做模 $2^n$ 加法：无论有符号还是无符号，溢出需要单独检测。&lt;/li&gt;
&lt;li&gt;“同号相加才会溢出”“异号相减可能溢出”是判断题常考语句。&lt;/li&gt;
&lt;li&gt;签扩展忘记复制符号位会导致算术右移错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;除法&lt;/h2&gt;
&lt;p&gt;设被除数为 $D$，除数为 $V$，均为 $n$ 位。硬件准备 $2n$ 位的余量寄存器 $R$ 与 $n$ 位的商寄存器 $Q$。&lt;/p&gt;
&lt;h3&gt;还原除法（Restoring Division）&lt;/h3&gt;
&lt;p&gt;每一轮执行“移-减-判断”三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;左移 $R$ 与 $Q$（等价于余量左移一位并引入下一位商）。&lt;/li&gt;
&lt;li&gt;试减：$R = R - V$。&lt;/li&gt;
&lt;li&gt;若 $R \geq 0$，则 $Q_0 = 1$；否则恢复 $R = R + V$，并置 $Q_0 = 0$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重复 $n$ 轮，得到 $n$ 位商；余量即 $R$。&lt;/p&gt;
&lt;h3&gt;非还原除法（Non-restoring）&lt;/h3&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;示例：13 除以 3（还原法）&lt;/h3&gt;
&lt;p&gt;初始化 $R = 0000_2$、$Q = 1101_2$（13），除数 $V = 0011_2$（3）。四轮迭代如下：&lt;/p&gt;
&lt;p&gt;| 轮次 | 左移后 $(R,Q)$ | 试减 $R - V$ | 判定 | 新 $(R,Q)$ |
| --- | --- | --- | --- | --- |
| 1 | $R=00001$, $Q=1010$ | $-0010$ | 负 -&gt; 还原，商位记 0 | $R=00001$, $Q=1010$ |
| 2 | $R=00011$, $Q=0100$ | $00000$ | 非负 -&gt; 商位记 1 | $R=00000$, $Q=0101$ |
| 3 | $R=00000$, $Q=1010$ | $-0011$ | 负 -&gt; 还原，商位记 0 | $R=00000$, $Q=1010$ |
| 4 | $R=00001$, $Q=0100$ | $-0010$ | 负 -&gt; 还原，商位记 0 | $R=00001$, $Q=0100$ |&lt;/p&gt;
&lt;p&gt;最终商 $Q = 0100_2 = 4$，余数 $R = 00001_2 = 1$，检验 $13 = 4 ,\times 3 + 1$。熟悉“移 -&gt; 试减 -&gt; 判定 -&gt; 还原/记 1”的顺序是掌握硬件除法的关键；非还原法则把“负值”分支改为加除数并在最后修正余量。&lt;/p&gt;
&lt;h3&gt;有符号除法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;步骤：对被除数和除数取绝对值执行无符号除法；商号位为 $\text{sign}(D) \oplus \text{sign}(V)$；余数与被除数同号。&lt;/li&gt;
&lt;li&gt;特例：$\text{INT_MIN} / -1$ 会溢出（结果超出表示范围）。&lt;/li&gt;
&lt;li&gt;RISC-V 在除数为 $0$ 时：&lt;code&gt;div&lt;/code&gt; 返回 $-1$，&lt;code&gt;divu&lt;/code&gt; 返回 all 1；&lt;code&gt;rem&lt;/code&gt; 返回被除数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;记忆点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;余量寄存器宽 $n+1$ 或 $n+2$ 位以容纳临时负值。&lt;/li&gt;
&lt;li&gt;每轮“移-试算”的顺序是考试高频题，必须牢记。&lt;/li&gt;
&lt;li&gt;结果校验：$D = Q \times V + R$，且 $0 \leq |R| &amp;#x3C; |V|$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;浮点数快速记忆（IEEE 754）&lt;/h2&gt;
&lt;h3&gt;字段布局与偏置&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;单精度 binary32：$S$ 1 bit、$E$ 8 bits、$F$ 23 bits，偏置 $\text{bias} = 127$。&lt;/li&gt;
&lt;li&gt;双精度 binary64：$S$ 1 bit、$E$ 11 bits、$F$ 52 bits，偏置 $\text{bias} = 1023$。&lt;/li&gt;
&lt;li&gt;尾数有效位：binary32 含隐含位共 $24$ 位，binary64 含 $53$ 位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数值表示&lt;/h3&gt;
&lt;p&gt;$$
V = (-1)^S \times M \times 2^{E - \text{bias}}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规格化数：$1 \leq M &amp;#x3C; 2$，形式 $1.F$。&lt;/li&gt;
&lt;li&gt;次正规数：$E = 0$，指数解释为 $1 - \text{bias}$，尾数形式 $0.F$。&lt;/li&gt;
&lt;li&gt;特殊编码：
&lt;ul&gt;
&lt;li&gt;$E = 0, F = 0$ =&gt; $\pm 0$。&lt;/li&gt;
&lt;li&gt;$E = 2^{k}-1, F = 0$ =&gt; $\pm\infty$。&lt;/li&gt;
&lt;li&gt;$E = 2^{k}-1, F \neq 0$ =&gt; NaN，最高小数位为 1 表示 quiet NaN。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;编码示例：&lt;/strong&gt; 将 $-5.75_{10}$ 编成 binary32。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;十进制转二进制：$-5.75 = -101.11_2$。&lt;/li&gt;
&lt;li&gt;规格化：$-1.0111_2 \times 2^2$ =&gt; $S = 1$、真实指数 $E_{\text{real}} = 2$。&lt;/li&gt;
&lt;li&gt;存储指数：$E = E_{\text{real}} + 127 = 129 = 1000,0001_2$。&lt;/li&gt;
&lt;li&gt;尾数字段取小数部分 $0111$ 并补足 23 位 =&gt; $0111,0000\ldots 0$。&lt;/li&gt;
&lt;li&gt;最终编码：&lt;code&gt;1 10000001 01110000000000000000000&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;二进制位示例&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正规数示例（binary32）&lt;/strong&gt;：$+0.15625 = 0.00101_2$。
&lt;ol&gt;
&lt;li&gt;规格化：$1.01_2 \times 2^{-3}$，指数存储为 $-3 + 127 = 124 = 0111,1100_2$。&lt;/li&gt;
&lt;li&gt;尾数字段：去掉隐含 1 得 $01000000000000000000000$。&lt;/li&gt;
&lt;li&gt;最终编码：&lt;code&gt;0 01111100 01000000000000000000000&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;次正规数示例&lt;/strong&gt;：binary32 的最小正值 $2^{-149}$。
&lt;ol&gt;
&lt;li&gt;编码：$S = 0$；指数字段全 0；尾数字段最低位为 1，其余 0。&lt;/li&gt;
&lt;li&gt;表示值：$0.F \times 2^{1-127} = 0.000\ldots 01_2 \times 2^{-126} = 2^{-149}$。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无穷与 NaN&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;$+\infty$：&lt;code&gt;0 11111111 00000000000000000000000&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;quiet NaN：&lt;code&gt;0 11111111 10000000000000000000000&lt;/code&gt;（最高小数位 1 表示 quiet）。&lt;/li&gt;
&lt;li&gt;signaling NaN：最高小数位 0、其余非零，例如 &lt;code&gt;0 11111111 01000000000000000000000&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;binary64 示例&lt;/strong&gt;：$-42.125 = -101010.001_2$。
&lt;ol&gt;
&lt;li&gt;规格化：$-1.01010001_2 \times 2^5$，指数存储为 $5 + 1023 = 1028 = 1000,0001,00_2$。&lt;/li&gt;
&lt;li&gt;尾数字段：$010100010000\ldots 0$（共 52 位）。&lt;/li&gt;
&lt;li&gt;最终编码：&lt;code&gt;1 1000000100 0101000100000000000000000000000000000000000000000000&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数值范围&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;binary32 暂存最小正规数 $2^{-126}$，最大约 $(2 - 2^{-23}) \times 2^{127}$。&lt;/li&gt;
&lt;li&gt;binary64 最小正规数 $2^{-1022}$，最大约 $(2 - 2^{-52}) \times 2^{1023}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;舍入模式与 GRS 位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;RISC-V 支持五种舍入：RNE（最近偶数）、RTZ（向 0）、RDN（向 $-\infty$）、RUP（向 $+\infty$）、RMM（最近到最大幅值）。&lt;/li&gt;
&lt;li&gt;GRS（Guard、Round、Sticky）位来自对齐时右移的信息，用于判断是否需要进位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;舍入模式详解（重点 RNE 向偶数）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GRS 位来源&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Guard ($G$)：当前保留尾数之后的第一位。&lt;/li&gt;
&lt;li&gt;Round ($R$)：第二位。&lt;/li&gt;
&lt;li&gt;Sticky ($S$)：剩余所有被丢弃位的或值；若后续有任意 1，则 $S = 1$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RNE 步骤&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;若 $G = 0$，直接截断（无进位）。&lt;/li&gt;
&lt;li&gt;若 $G = 1$ 且 ($R = 1$ 或 $S = 1$)，向上进位。&lt;/li&gt;
&lt;li&gt;若仅 $G = 1$ 且 $R = S = 0$（恰好在中间值，即 $0.5$ ULP），检查当前最低保留位 $L$：
&lt;ul&gt;
&lt;li&gt;$L = 0$（偶数），保持不变；&lt;/li&gt;
&lt;li&gt;$L = 1$（奇数），加 1 使其变偶。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：binary32 尾数保留 24 位，现有 $M_{\text{raw}} = 1.01010101010101010101011_2$。
&lt;ul&gt;
&lt;li&gt;$G = 1$（第 25 位），$R = 0$，$S = 0$，最低保留位 $L = 1$。&lt;/li&gt;
&lt;li&gt;条件 3 触发，进位后得到 $1.0101010101010101010110_2$，最低位变 0（偶数）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与其他模式比较&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;RTZ：直接舍弃，结果总是趋向 0。&lt;/li&gt;
&lt;li&gt;RDN/RUP：根据符号选择向下或向上。&lt;/li&gt;
&lt;li&gt;RMM：最近且偏向绝对值大的方向，遇到 0.5 ULP 总是进位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;异常与标志&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;5 种 IEEE 754 异常：invalid、division-by-zero、overflow、underflow、inexact。&lt;/li&gt;
&lt;li&gt;RISC-V 使用 &lt;code&gt;fflags&lt;/code&gt; 记录异常，通过 &lt;code&gt;frm&lt;/code&gt; 设置默认舍入模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;浮点运算流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加减&lt;/strong&gt;：对齐指数 -&gt; 处理尾数符号 -&gt; 规格化 -&gt; 舍入 -&gt; 记录异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;乘法&lt;/strong&gt;：
$$
E_{\text{out}} = E_A + E_B - \text{bias}
$$
$$
M_{\text{out}} = M_A \times M_B
$$
若尾数 $M_{\text{out}} \in [2,4)$ 需右移并指数加一；若溢出 -&gt; $\pm\infty$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;除法&lt;/strong&gt;：
$$
E_{\text{out}} = E_A - E_B + \text{bias}
$$
$$
M_{\text{out}} = M_A / M_B
$$
通过迭代除法或牛顿迭代获得尾数，再规格化与舍入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;加法示例&lt;/strong&gt;：计算 binary32 的 $1.5 + (-2.25)$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写成规格化形式：$1.5 = 1.1_2 ,\times 2^0$，$-2.25 = -1.001_2 ,\times 2^1$。&lt;/li&gt;
&lt;li&gt;对齐指数：右移较小指数的尾数，即 $1.1_2 \rightarrow 0.11_2$ 并指数加 1。&lt;/li&gt;
&lt;li&gt;尾数相加：$0.11_2 + (-1.001_2) = -0.111_2$。&lt;/li&gt;
&lt;li&gt;规格化：$-1.11_2 ,\times 2^0$。&lt;/li&gt;
&lt;li&gt;舍入：尾数正好符合精度 =&gt; 结果 $-0.75$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;浮点误差提示&lt;/strong&gt;：尾数位数有限 =&gt; 每次舍入最多引入 $0.5$ ULP（单位最后位）；累积误差可能导致“不满足结合律”。&lt;/p&gt;
&lt;h3&gt;速记列表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;字段尺寸：binary32 是 $1/8/23$，binary64 是 $1/11/52$。&lt;/li&gt;
&lt;li&gt;偏置：binary32 = 127，binary64 = 1023。&lt;/li&gt;
&lt;li&gt;有效位：binary32 -&gt; 24，binary64 -&gt; 53。&lt;/li&gt;
&lt;li&gt;五种舍入 + 五类异常必须背熟。&lt;/li&gt;
&lt;li&gt;特殊值识别：$E = 0$ =&gt; $0$ 或次正规；$E = \text{all 1}$ =&gt; $\infty$ 或 NaN。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RISC‑V 指令速记&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;乘法（M 扩展）：&lt;code&gt;mul&lt;/code&gt;（低位），&lt;code&gt;mulh&lt;/code&gt;（有符号高位），&lt;code&gt;mulhu&lt;/code&gt;（无符号高位），&lt;code&gt;mulhsu&lt;/code&gt;（混合）。&lt;/li&gt;
&lt;li&gt;除法/取余（M 扩展）：&lt;code&gt;div&lt;/code&gt;，&lt;code&gt;divu&lt;/code&gt;，&lt;code&gt;rem&lt;/code&gt;，&lt;code&gt;remu&lt;/code&gt;；除数为 0 的行为依规范定义。&lt;/li&gt;
&lt;li&gt;浮点（F/D 扩展）：加减乘除与转换指令；舍入模式与异常通过 CSR 控制/记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;速记清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;补码范围：无符号 $0 \to 2^n-1$，有符号 $-2^{n-1} \to 2^{n-1}-1$；溢出检测 $c_n \oplus c_{n-1}$。&lt;/li&gt;
&lt;li&gt;乘法：部分积宽 $2n$；Booth 表 $(01\rightarrow +M, 10\rightarrow -M)$；CSA 在部分积累加阶段使用。&lt;/li&gt;
&lt;li&gt;除法：记“移 -&gt; 试减 -&gt; 判定 -&gt; 还原/记 1”；结果满足 $D = QV + R$。&lt;/li&gt;
&lt;li&gt;浮点：binary32 = 1/8/23、bias 127；binary64 = 1/11/52、bias 1023；舍入模式 RNE/RTZ/RDN/RUP/RMM。&lt;/li&gt;
&lt;li&gt;特殊值：$E=0, F=0$ =&gt; $\pm 0$；$E=0, F\neq0$ =&gt; 次正规；$E=\text{all 1}, F=0$ =&gt; $\pm\infty$；$E=\text{all 1}, F\neq0$ =&gt; NaN。&lt;/li&gt;
&lt;li&gt;RISC-V M 扩展：&lt;code&gt;mul&lt;/code&gt;, &lt;code&gt;mulh&lt;/code&gt;, &lt;code&gt;mulhu&lt;/code&gt;, &lt;code&gt;mulhsu&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;divu&lt;/code&gt;, &lt;code&gt;rem&lt;/code&gt;, &lt;code&gt;remu&lt;/code&gt;；浮点异常在 &lt;code&gt;fflags&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>指令：计算机的语言</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap2</guid><description>武汉大学2025秋季学期 计算机组成 第二章</description><pubDate>Tue, 30 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chapter 2: Instructions: Language of the Computer&lt;/p&gt;
&lt;p&gt;本章围绕 RISC-V 指令集展开。&lt;/p&gt;
&lt;p&gt;RISC-V 是一种&lt;strong&gt;精简指令集&lt;/strong&gt;计算机架构，适用于嵌入式系统。&lt;/p&gt;
&lt;h2&gt;ISA&lt;/h2&gt;
&lt;p&gt;计算机 ISA 必须体现的三个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算指令：算数运算和逻辑运算&lt;/li&gt;
&lt;li&gt;传送指令：CPU 需要和存储器、IO 交换数据&lt;/li&gt;
&lt;li&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;操作码 OPCode，代表指令的类型。&lt;/li&gt;
&lt;li&gt;操作数 Operands，表示指令的具体操作对象，个数不定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于操作数个数不定，指令格式也不唯一，分为变长指令和定长指令。&lt;/p&gt;
&lt;h2&gt;RISC-V&lt;/h2&gt;
&lt;p&gt;RISC-V 采用固定长度的 32 位指令格式。&lt;/p&gt;
&lt;h3&gt;设计原则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simplicity favours regularity——使用定长指令&lt;/li&gt;
&lt;li&gt;Smaller is faster——使用较少的指令类型、寄存器数量&lt;/li&gt;
&lt;li&gt;Good design demands good compromises——在设计中做出权衡，让指令格式尽可能简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;指令组成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;操作码&lt;/strong&gt;：用最低 7 位存储操作码，所以只能表示 128 种不同的指令，可以通过扩展技术来增加指令数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作数&lt;/strong&gt;：最多可以有三个操作数，共有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寄存器 Register：
&lt;ul&gt;
&lt;li&gt;RISC-V 有 32 个寄存器，名字从 x0 到 x31。&lt;/li&gt;
&lt;li&gt;每个寄存器存储 32 位数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;立即数 Immediate：用二进制补码表示。&lt;/li&gt;
&lt;li&gt;存储器 Memory：
&lt;ul&gt;
&lt;li&gt;以字节为单位寻址(8-bit)，每个存储单元有一个 32 位的地址。&lt;/li&gt;
&lt;li&gt;RISC-V is Little Endian.&lt;/li&gt;
&lt;li&gt;RISC-V 不要求存储器对齐(alignment)，但对齐可以提高性能。&lt;/li&gt;
&lt;li&gt;在指令中，用 address + offset 的方式访问 memory，其中 address 由寄存器提供，offset 由立即数提供。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;指令格式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/img-03.png&quot; alt=&quot;image-20250923191504737&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;R-type&lt;/strong&gt;：三个操作数都是寄存器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rd 是目的寄存器，rs1 和 rs2 是源寄存器。&lt;/li&gt;
&lt;li&gt;funct3 和 funct7 用于指令扩展——区分同一 opcode 下的不同指令。&lt;/li&gt;
&lt;li&gt;包括所有运算指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;I-type&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;load 指令&lt;/strong&gt;：把存储器中的数据加载到寄存器中。
&lt;ul&gt;
&lt;li&gt;lw, lh, lb 分别加载 4 字节、2 字节、1 字节数据，空缺的高位用符号位扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;S-type&lt;/strong&gt;：有一个立即数和两个寄存器，没有目的操作数 rd，用于存储。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;store 指令&lt;/strong&gt;：把寄存器中的数据存储到存储器中，不需要符号扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;U-type&lt;/strong&gt;：用于把一个 20 位的立即数加载到寄存器的高 20 位，低 12 位清零。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;lui 指令&lt;/strong&gt;：弥补了其他指令只能加载 12 位立即数的不足&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;auipc 指令&lt;/strong&gt;：把 pc 加上一个 20 位的立即数，结果存储到寄存器中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lui 和 auipc 的区别是，前者是赋值，后者是加法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;B-type&lt;/strong&gt;：用于&lt;strong&gt;条件分支指令&lt;/strong&gt;，有两个寄存器和一个立即数，没有目的操作数 rd。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据两个寄存器的值决定，是否跳转到 pc + offset 处执行。&lt;/li&gt;
&lt;li&gt;offset 是一个有符号数，表示相对于当前 pc 的偏移量，单位是字节，会加到 pc 上。&lt;/li&gt;
&lt;li&gt;RISC-V 中，要求 offset 必须是 2 的倍数，所以最低位总是 0，不用存储在指令中，因此参考上面的图片，offset 截成多段存储，只用 12 个位，但范围是 $-2^{12}\sim 2^{12}-1$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;J-type&lt;/strong&gt;：用于&lt;strong&gt;无条件跳转指令&lt;/strong&gt;，有一个寄存器和一个立即数，没有源操作数 rs2。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jal 指令：把 pc + 4 存储到 rd 中，然后跳转到 pc + offset 处执行。offset 范围是 $-2^{20}\sim 2^{20}-1$，也是因为最低位是 0，不用存储在指令中。&lt;/li&gt;
&lt;li&gt;jalr 指令：TODO .把 pc + 4 存储到 rd 中，然后跳转到 rs1 + offset 处执行。跳转范围更大，因为寄存器可以有很大范围的赋值。&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;p&gt;在 x86 中，原子指令通常通过&lt;strong&gt;锁&lt;/strong&gt;来实现，锁住总线，使CPU无法与IO和Memory通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RISC-V 中的原子指令&lt;/strong&gt;比较轻量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load reserved: lr.w rd, (rs1)，其中 rs1 是要读取的地址，rd 是读取的目的寄存器。同时会把地址保留在硬件维护的&lt;strong&gt;保留集&lt;/strong&gt;中。&lt;/li&gt;
&lt;li&gt;Store conditional: sc.w rd, (rs1)，其中 rs2，rs1 是要写入的地址，rs2 是要写入的数据，&lt;strong&gt;rd 是结果寄存器，如果写入失败则 rd = 1&lt;/strong&gt;。只有当该地址仍在当前硬件维护的&lt;strong&gt;保留集&lt;/strong&gt;中，且是有效的（未被其他线程修改），才成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样就能在不加锁的情况下完成安全的原子读-改-写操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;保留集&lt;/strong&gt;：是硬件维护的一个表，程序员无法访问。它记录地址及其状态。如果中途被修改了，监控器会修改保留集的状态，导致后续的 sc.w 失败。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编写 RISC-V 汇编代码，使用 lr.w / sc.w 指令将以下 C 代码实现为原子的 “set max” 操作。这里，参数 shvar 包含共享变量的地址，如果 x 大于它指向的值，则应该用 x 替换：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void setmax(int* shvar, int x) {
    // 临界区开始
    if (x &gt; *shvar)
        *shvar = x;
    // 临界区结束
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;setmax:
    lr.d t0, (a0)
    bge t0, a1, done
    sc.d t1, a1, (a0)
done:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在 lr.d 之后，t0 被其他进程修改了，那么就自己结束报错。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>计算机抽象与技术</title><link>https://www.tonyyin0418.com/blog/computer-org/co-chap1</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/computer-org/co-chap1</guid><description>武汉大学2025秋季学期 计算机组成 第一章</description><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chapter 1: Computer Abstractions and Technology&lt;/p&gt;
&lt;p&gt;本章关注两个话题——&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算机设计的八大思想、计算机系统的概念&lt;/li&gt;
&lt;li&gt;设计单处理器计算机系统，如何衡量性能指标&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;计算机系统&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;计算机科学的发展&lt;/strong&gt;：几十年来，在 Moore&apos;s Law 摩尔定律的推动下，计算机的性能和复杂性在过去的几十年里有了显著提升，驱使人工智能技术的进步。这一定律关注集成度，也就是芯片上晶体管的数量、密度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算机分类&lt;/strong&gt;：按大小，分为——个人计算机、服务器、高性能计算机、嵌入式计算机。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Machine Structure&lt;/strong&gt;：运算器（Datapath）、控制器（Control）、存储器、输入、输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-01.png&quot; alt=&quot;image-20250909192941479&quot;&gt;&lt;/p&gt;
&lt;p&gt;其中连接软件和硬件的接口是黄色部分：ISA（Instruction Set Architecture，指令集架构）是机器结构的一个重要方面，定义了程序员与计算机硬件之间的接口。只有设计好了ISA，才能设计出计算机硬件。（第二章）&lt;/p&gt;
&lt;p&gt;常见的指令架构包括：RISC-V，x86-64，ARM，Loongarch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What you will learn&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How programs are translated into machine language.&lt;/li&gt;
&lt;li&gt;The hardware designers improve&lt;/li&gt;
&lt;li&gt;What determines program performance&lt;/li&gt;
&lt;li&gt;How to improve program performance&lt;/li&gt;
&lt;li&gt;parallel processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;计算机设计的八大思想&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Design for &lt;strong&gt;Moore&apos;s Law&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;abstraction&lt;/strong&gt; to simplify design：可以让同一个程序在不同的硬件上运行，只与ISA有关。&lt;/li&gt;
&lt;li&gt;Make the &lt;strong&gt;common case fast&lt;/strong&gt;：让加法器最快，因为加法是最常用的。&lt;/li&gt;
&lt;li&gt;Performance vis &lt;strong&gt;parallelism&lt;/strong&gt;：并行&lt;/li&gt;
&lt;li&gt;Performance via &lt;strong&gt;pipelineing&lt;/strong&gt;：流水线&lt;/li&gt;
&lt;li&gt;Performance via &lt;strong&gt;prediction&lt;/strong&gt;：预测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchy&lt;/strong&gt; of memories： 存储器的层次结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependability&lt;/strong&gt; via redundacy： 冗余&lt;/li&gt;
&lt;/ul&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;h2&gt;性能评价&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Throughput 吞吐量&lt;/strong&gt;：单位时间完成的工作量。这对单CPU计算机没有意义，只对于并行计算机有意义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Elapsed time&lt;/strong&gt;：完成一个工作所需要总的时间，包括等待时间，所以不能完全体现CPU的性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU time&lt;/strong&gt;：程序在 CPU 上实际消耗的时间，不含等待时间。&lt;/p&gt;
&lt;p&gt;CPU 具有时钟周期，是固定的时间间隔 &lt;strong&gt;Clock cycle time&lt;/strong&gt;，由 CPU Frequency 主频决定&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/img-02.png&quot; alt=&quot;image-20250911105100806&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clock period&lt;/strong&gt; 和 &lt;strong&gt;Clock Frequency&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;$1,\text{ps}=1\times 10^{-12},\text{s}$.&lt;/li&gt;
&lt;li&gt;1 Ghz 表示 $10^9$ 个周期每秒，周期时间为 $1,\text{ns}=10^{-9},\text{s}$.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clock period&lt;/strong&gt; 的倒数是 &lt;strong&gt;Clock Frequency&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每条指令需要若干个时钟周期来完成，所以可以统计总周期数来计算 CPU time：
$$
\text{CPU time} = \text{CPU clock cycles} \times \text{Clock cycle time}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主频提高会导致周期数增加， 所以在设计 CPU 时需要考虑折中二者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而，计算 Clock cycle time 较为困难，因此我们引入 &lt;strong&gt;Cycles Per Instruction, CPI&lt;/strong&gt;，表示每条指令平均需要多少个时钟周期，则：
$$
\text{CPU time} = (\text{Instruction count} \times \text{CPI})\times \text{Clock cycle time}
$$
CPI 不止与指令集相关，ISA 相同只代表 Instruction count 相同。&lt;/p&gt;
&lt;p&gt;RISC-V 是 RISC 简单指令集，每条指令周期数差别不大，然而例如 x86 等 CISC 指令集的指令周期数差别较大，继续使用 CPI 计算 CPU time 不太适用，因此分类计算，将 CPI 拆分为各类指令的 CPI 乘以该类指令的数量之和，得到更准确的计算公式：&lt;/p&gt;
&lt;p&gt;$$
\text{CPU time} = \left(\sum_{i=1}^{n} \text{Instruction count}_i \times \text{CPI}_i\right)\times \text{Clock cycle time}
$$&lt;/p&gt;
&lt;p&gt;影响性能的因素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Algorithm: affects IC, possibly CPI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Programming language: affects IC, CPI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compiler: affects IC, CPI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Instruction set architecture: affects IC, CPI, T_c&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;谬误&lt;/h3&gt;
&lt;p&gt;提升程序部分的性能并不能提升整体性能。&lt;/p&gt;
&lt;p&gt;Amdahl&apos;s Law：如果一个程序的某个部分加速了 S 倍，而该部分占总运行时间的 F，那么加速后的总运行时间为：
$$
\text{New Time} = \text{Old Time} \times \left((1-F) + \frac{F}{S}\right)
$$&lt;/p&gt;</content:encoded></item><item><title>假设检验</title><link>https://www.tonyyin0418.com/blog/probability/chap-8</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-8</guid><description>武汉大学2025春季学期 概率论与数理统计 第八章 假设检验</description><pubDate>Tue, 03 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&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;strong&gt;构造合适的统计量&lt;/strong&gt;，对假设进行判断真伪，称为假设检验。&lt;/p&gt;
&lt;p&gt;提出的&lt;strong&gt;原假设&lt;/strong&gt;记为 $H_0$，与之相反的&lt;strong&gt;备择假设&lt;/strong&gt;记为 $H_1$.&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;strong&gt;由于小概率事件在一次试验中几乎不可能发生，所以如果发生了，则有理由认为假设不成立。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拒真错误&lt;/strong&gt;：$H_0$ 实际正确，拒绝 $H_0$；&lt;strong&gt;取伪错误&lt;/strong&gt;：$H_0$ 实际错误，接受 $H_0$.&lt;/p&gt;
&lt;h2&gt;显著性检验&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;设总体 $X\sim N(\mu,\sigma^2)$，$\sigma$ 已知，$x_1,x_2\cdots,x_n$ 为来自 $X$ 的样本值，现对 $\mu$ 进行假设检验。&lt;/p&gt;
&lt;p&gt;试说明在显著性水平 $\alpha=0.05$ 下，关于 $\mu$ 的检验问题 $H_0:\mu=\mu_0$，$H_1:\mu\neq \mu_0$ 的拒绝域为 $|z|=|\dfrac{\bar x-\mu_0}{\sigma/\sqrt{n}}|&gt;z_{\alpha/2}$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里&lt;strong&gt;显著性水平&lt;/strong&gt; $\alpha$ 表示的是：&lt;strong&gt;拒真错误的概率&lt;/strong&gt;，是小概率事件。&lt;/p&gt;
&lt;p&gt;先认为 $H_0$ 是正确的，则 $X\sim N(\mu_0,\sigma^2)$，有：
$$
\bar X\sim N(\mu_0, \frac{\sigma^2}{n})\quad \Rightarrow \frac{\bar X-\mu_0}{\sigma/\sqrt{n}}\sim N(0,1)
$$
然后结合正态分布的图像，可知拒绝域为两侧：
$$
p\left{\left|\frac{\bar x-\mu_0}{\sigma/\sqrt{n}}\right|&gt;z_{\alpha/2}\right}=\alpha \quad\Rightarrow|z|=|\frac{\bar x-\mu_0}{\sigma/\sqrt{n}}|&gt;z_{\alpha/2}
$$
在实际问题中，把 $\bar x$ 等数值代入，就可以判断这个观察值是否在小概率区间（拒绝域），从而判断假设真伪。&lt;/p&gt;
&lt;h2&gt;正态总体的假设检验&lt;/h2&gt;
&lt;p&gt;| $H_0$                                                        | 枢轴量                                                       | $H_1$                                                        | 拒绝域                                                       |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| $$\begin{aligned}\mu\leq \mu_0 \ \mu\geq \mu_0 \ \mu=\mu_0\end{aligned}$$$\sigma^2$已知 | $\displaystyle Z = \frac{\bar{X} - \mu}{\sigma/\sqrt{n}} \sim N(0,1)$ | $$\begin{aligned}\mu&gt; \mu_0 \ \mu&amp;#x3C; \mu_0 \ \mu\neq\mu_0\end{aligned}$$ | $$\begin{aligned}z\geq z_\alpha \ z\leq -z_{\alpha} \ \lvert z\rvert\geq z_{\alpha/2}\end{aligned}$$ |
| $$\begin{aligned}\mu\leq \mu_0 \ \mu\geq \mu_0 \ \mu=\mu_0\end{aligned}$$$\sigma^2$未知 | $\displaystyle T = \frac{\bar{X} - \mu}{S/\sqrt{n}} \sim t(n-1)$ | $$\begin{aligned}\mu&gt; \mu_0 \ \mu&amp;#x3C; \mu_0 \ \mu\neq\mu_0\end{aligned}$$ | $$\begin{aligned}t\geq t_\alpha(n-1) \ t\leq -t_{\alpha}(n-1) \ \lvert t\rvert\geq t_{\alpha/2}(n-1)\end{aligned}$$ |
| $$\begin{aligned}\sigma^2\leq \sigma^2_0 \ \sigma^2\geq \sigma^2_0 \ \sigma^2=\sigma^2_0\end{aligned}$$$\sigma^2$已知 | $\displaystyle \frac{(n-1)S^2}{\sigma^2}\sim \chi^2(n-1)$    | $$\begin{aligned}\sigma^2&gt; \sigma^2_0 \ \sigma^2&amp;#x3C; \sigma^2_0 \ \sigma^2\neq\sigma^2_0\end{aligned}$$ | $$\begin{aligned}\chi^2\geq \chi^2_\alpha(n-1) \ \chi^2\leq \chi^2_{1-\alpha}(n-1) \ \chi^2\geq \chi^2_{\alpha/2}(n-1)\text{ OR } \chi^2\leq \chi^2_{1-\alpha/2}(n-1)\end{aligned}$$ |&lt;/p&gt;</content:encoded></item><item><title>异常控制流 II</title><link>https://www.tonyyin0418.com/blog/csapp/ecf-procs2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/ecf-procs2</guid><description>武汉大学2025春季学期 计算机系统基础 第八章 异常控制流 II</description><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Shell&lt;/h2&gt;
&lt;p&gt;A shell is an application program that runs programs on behalf of the user.&lt;/p&gt;
&lt;p&gt;在 Linux 中，shell 是一个命令行界面，用户可以通过 shell 输入命令，shell 会将这些命令传递给操作系统执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;csapp.h&quot;
#define MAXARGS   128

/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv); 

int main() 
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
	/* Read */
	printf(&quot;&gt; &quot;);                   
	Fgets(cmdline, MAXLINE, stdin); 
	if (feof(stdin))
	    exit(0);

	/* Evaluate */
	eval(cmdline);
    } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个最简单的 shell 程序，功能是：&lt;strong&gt;读取用户输入的命令行，然后执行这个命令行。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* eval - Evaluate a command line */
void eval(char *cmdline) 
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? */
    pid_t pid;           /* Process id */
    
    strcpy(buf, cmdline);
    bg = parseline(buf, argv); 
    if (argv[0] == NULL)  
	return;   /* Ignore empty lines */

    if (!builtin_command(argv)) { 
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) &amp;#x3C; 0) {
                printf(&quot;%s: Command not found.\n&quot;, argv[0]);
                exit(0);
            }
        }

	/* Parent waits for foreground job to terminate */
	if (!bg) {
	    int status;
	    if (waitpid(pid, &amp;#x26;status, 0) &amp;#x3C; 0)
		unix_error(&quot;waitfg: waitpid error&quot;);
	}
	else
	    printf(&quot;%d %s&quot;, pid, cmdline);
    }
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;eval&lt;/code&gt; 函数中，首先调用 &lt;code&gt;parseline&lt;/code&gt; 函数解析命令行，将命令行分解为一个个参数，并存储在 &lt;code&gt;argv&lt;/code&gt; 数组中。之后根据第一个参数，分类执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是内置命令（如 &lt;code&gt;quit&lt;/code&gt;），则调用 &lt;code&gt;builtin_command&lt;/code&gt; 函数处理这个命令。&lt;/li&gt;
&lt;li&gt;是可执行目标文件，则调用 &lt;code&gt;Fork&lt;/code&gt; 函数创建一个子进程，然后在子进程中调用 &lt;code&gt;execve&lt;/code&gt; 函数执行这个命令。&lt;/li&gt;
&lt;li&gt;然后根据 &lt;code&gt;bg&lt;/code&gt; 变量判断这个命令是前台进程还是后台进程，如果是前台进程，则调用 &lt;code&gt;waitpid&lt;/code&gt; 函数等待子进程结束；如果是后台进程，则打印出子进程的 PID 和命令行，不等待子进程结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个简单 shell 的缺陷是：终端作为父进程，并不知道子进程何时终止结束，无法回收子进程的资源。&lt;/p&gt;
&lt;p&gt;由此需要引入 Signals。&lt;/p&gt;
&lt;h2&gt;Signals&lt;/h2&gt;
&lt;p&gt;这个问题用需要之前学习过的 ECF 来解决，这部分对于实现 shell 非常重要。&lt;/p&gt;
&lt;p&gt;A**「signal」**is a small message that notifies a process that an event some type has occurred in the system.&lt;/p&gt;
&lt;p&gt;是由&lt;strong&gt;内核&lt;/strong&gt;发送给&lt;strong&gt;进程&lt;/strong&gt;的一个消息，用 $1\sim 30$ 的小整数表示，代表不同含义。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体信号含义可以用 &lt;code&gt;man 7 signal&lt;/code&gt; 查看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Sending a Signal&lt;/h3&gt;
&lt;p&gt;发送信号。从 Kernel 到 Destination process 传递，改变进程 **上下文（Context）**中的某些状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Process Group 进程组」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程都要属于，也只属于一个进程组，进程组也有 ID。&lt;/li&gt;
&lt;li&gt;默认情况下，子进程和他的父进程同属于一个进程组。&lt;/li&gt;
&lt;li&gt;可以用 &lt;code&gt;getpgrp()&lt;/code&gt; 获取进程组，用 &lt;code&gt;setpgrid()&lt;/code&gt; 改变进程组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「/bin/kill 程序」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是系统提供的一个命令行工具，用来发送信号给进程。&lt;/li&gt;
&lt;li&gt;它的名字是 kill，但它并不一定会杀死进程，&lt;strong&gt;可以发送任何指定信号给任何进程。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/bin/kill -9 15213&lt;/code&gt; 中，信号 9 是 SIGKILL，15213 是&lt;strong&gt;进程 PID&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/bin/kill -9 -15213&lt;/code&gt; 中，信号 9 是 SIGKILL，15213 是&lt;strong&gt;进程组 ID&lt;/strong&gt;，会杀死这个组中的每个进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「Keyboard」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入 &lt;code&gt;Ctrl+C&lt;/code&gt; 会发送 &lt;code&gt;SIGINT&lt;/code&gt; 信号，终止前台进程组的所有作业。&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;Ctrl+Z&lt;/code&gt; 会发送 &lt;code&gt;SIGTSTP&lt;/code&gt; 信号，挂起前台进程组的所有作业。&lt;/li&gt;
&lt;li&gt;前台进程组中，可能会有多个进程在运行，比如指令 &lt;code&gt;ls | sort&lt;/code&gt; 就会创建两个进程，分别是 &lt;code&gt;ls&lt;/code&gt; 和 &lt;code&gt;sort&lt;/code&gt;，它们同属于前台作业的进程组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「kill() Function」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C 语言中提供了 &lt;code&gt;kill()&lt;/code&gt; 函数，可以用来发送信号给进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$$
\mathtt{int}\quad \mathtt{kill(pid_t;;pid, int;; sig)}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个跟 &lt;code&gt;/bin/kill&lt;/code&gt; 程序一样，都用正负来区分进程和进程组。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Receiving a Signal&lt;/h3&gt;
&lt;p&gt;接收信号。&lt;/p&gt;
&lt;p&gt;进程被内核要求对信号做出反应后，有以下几种情况可以选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**「Ignore」**直接忽略，什么都不做&lt;/li&gt;
&lt;li&gt;**「Default」**根据信号类型的预定义动作处理，比如终止、忽略、停止。&lt;/li&gt;
&lt;li&gt;**「Catch」**执行一个叫做 &lt;strong&gt;signal handler&lt;/strong&gt; 的函数，来处理这个信号，如下图所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./ecf-procs2-01.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以，对于大部分信号，进程可以自己选择：直接忽略，默认处理，或者自定义处理函数。&lt;/p&gt;
&lt;p&gt;对于特殊信号 &lt;code&gt;SIGKILL&lt;/code&gt; 和 &lt;code&gt;SIGSTOP&lt;/code&gt;，进程不能选择忽略或者捕获，必须终止或者停止。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void handler(int sig) {
    printf(&quot;Received signal %d\n&quot;, sig);
    exit(0);
}
signal(SIGINT, handler); // 大致实现逻辑如上
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pending and Blocked Signals&lt;/h3&gt;
&lt;p&gt;这个过程是接收信号之前，信号会被放在一个 Pending 队列中，等待进程处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Pending」&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;strong&gt;最多只能有一个&lt;/strong&gt; 处于 Pending 状态的信号。比如连续发送多次同一个型号 &lt;code&gt;SIGINT&lt;/code&gt;，也并不会堆积起来，而是只会保留一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「Block」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号被阻塞，进程不会接收这个信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocked signals can be delivered, but will not be received until the signal is unblocked&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以就是说，如果信号处于 Blocked 状态，就会持续处于 Pending 状态，直到进程解除 Blocked 状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Bit Vector」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blocked signals 用一个 Bit Vector 来表示，表示每个信号是否被 Blocked。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pending bit vector&lt;/strong&gt;：当某个信号 k 被内核投递给进程时，&lt;strong&gt;内核设置 pending 中的第 k 位为 1&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocked bit vector&lt;/strong&gt;：当某个信号 k 被屏蔽时，&lt;strong&gt;内核设置 blocked 中的第 k 位为 1&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Ready = pending &amp;#x26; ~blocked，才会被接收。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「实现方法」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sigprocmask()&lt;/code&gt; 函数可以用来设置 Blocked signals。&lt;/p&gt;
&lt;p&gt;具体用法可以自行查询，&lt;code&gt;man 2 sigprocmask&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一般是先 &lt;code&gt;sigprocmask(SIG_BLOCK, &amp;#x26;mask, &amp;#x26;prev_mask)&lt;/code&gt; 来阻塞，&lt;/p&gt;
&lt;p&gt;然后 &lt;code&gt;sigprocmask(SIG_SETMASK, &amp;#x26;prev_mask, NULL)&lt;/code&gt; 来恢复。&lt;/p&gt;</content:encoded></item><item><title>参数估计</title><link>https://www.tonyyin0418.com/blog/probability/chap-7</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-7</guid><description>武汉大学2025春季学期 概率论与数理统计 第七章 参数估计</description><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第七章：参数估计。&lt;/p&gt;
&lt;p&gt;这是统计推断类问题的一部分，另一部分是假设检验问题。&lt;/p&gt;
&lt;p&gt;参数估计的目标是：&lt;strong&gt;用样本数据，去估计总体的某个未知参数&lt;/strong&gt;，常见为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总体均值 $\mu$，总体方差 $\sigma^2$，总体比例 $p$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;点估计&lt;/h2&gt;
&lt;p&gt;设总体 $X$ 的分布函数&lt;strong&gt;形式已知&lt;/strong&gt;，但是总体参数未知，要用一个样本来估计参数。&lt;/p&gt;
&lt;p&gt;形式化地，设总体参数为 $\theta$，则用样本算出一个统计量 $ \hat{\theta}$ 来估计它。&lt;/p&gt;
&lt;h3&gt;矩估计&lt;/h3&gt;
&lt;p&gt;优点：推导简单直观，不依赖复杂的极大似然函数；&lt;/p&gt;
&lt;p&gt;缺点：在某些分布下可能效率较低，不如最大似然估计（MLE）稳定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「步骤」&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设总体的第 $k$ 阶原点矩为 $\mu&apos;_k = E[X^k]$，这个式子会带有总体中的未知参数。&lt;/li&gt;
&lt;li&gt;样本的第 $k$ 阶样本矩为：
$$
m_k = \frac{1}{n} \sum_{i=1}^n X_i^k
$$&lt;/li&gt;
&lt;li&gt;将前 $r$ 个样本矩 $m_1, m_2, \dots, m_r$ 分别等于总体矩 $\mu&apos;_1, \mu&apos;_2, \dots, \mu&apos;_r$，解方程组得到 $r$ 个未知参数的估计量 $\hat{\theta}_1, \dots, \hat{\theta}_r$。&lt;/li&gt;
&lt;li&gt;最后选择合适的估计量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;「举例：指数分布的矩估计」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设总体 $X \sim \text{Exp}(\lambda)$，我们希望通过样本来估计参数 $\lambda$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤如下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第 1 步：写出总体矩（含参数）&lt;/p&gt;
&lt;p&gt;指数分布的一阶原点矩为：
$$
\mu_1&apos; = E[X] = \frac{1}{\lambda}
$$&lt;/p&gt;
&lt;p&gt;第 2 步：计算样本矩（来自样本数据）&lt;/p&gt;
&lt;p&gt;样本的一阶样本矩即为样本均值：
$$
m_1 = \bar{X} = \frac{1}{n} \sum_{i=1}^n X_i
$$&lt;/p&gt;
&lt;p&gt;第 3 步：令&lt;strong&gt;样本矩 = 总体矩&lt;/strong&gt;，解出参数估计量&lt;/p&gt;
&lt;p&gt;令 $\bar{X} = \frac{1}{\lambda}$，可得：
$$
\hat{\lambda}_{\text{MM}} = \frac{1}{\bar{X}}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;设总体 $X$ 在 $[a,b]$ 上服从均匀分布，$a,b$ 未知，$X_1, X_2, \cdots,X_n$ 是来自 $X$ 的样本，求 $a, b$ 的矩估计量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原点矩：
$$
\mu_1&apos; = E[X] = \frac{a + b}{2}
$$&lt;/p&gt;
&lt;p&gt;$$
\mu_2&apos; = E[X^2] = D(X)+[E(X)]^2=\frac{(b-a)^2}{12}+\frac{(a+b)^2}{4}
$$&lt;/p&gt;
&lt;p&gt;样本矩：
$$
A_1 = \bar{X} = \frac{1}{n} \sum_{i=1}^n X_i
$$&lt;/p&gt;
&lt;p&gt;$$
A_2 = \frac{1}{n} \sum_{i=1}^n X_i^2
$$&lt;/p&gt;
&lt;p&gt;联立等式：
$$
\begin{cases}
\frac{a + b}{2} = \bar{X} \
\frac{a^2 + ab + b^2}{3} = A_2
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;整理得到最终结果：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\hat{a} = \bar{X} - \sqrt{3(A_2 - \bar{X}^2)} \
\hat{b} = \bar{X} + \sqrt{3(A_2 - \bar{X}^2)}
\end{aligned}
$$&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;极大似然估计&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;/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;设样本 $X_1, X_2, \dots, X_n$ 独立同分布，密度函数为 $f(x;\theta)$，其中 $\theta$ 为未知参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;写出&lt;strong&gt;似然函数&lt;/strong&gt;：
$$
L(\theta) = \prod_{i=1}^n f(X_i; \theta)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通常取对数化简，得到&lt;strong&gt;对数似然函数&lt;/strong&gt;：
$$
\ell(\theta) = \log L(\theta) = \sum_{i=1}^n \log f(X_i; \theta)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对 $\ell(\theta)$ 对 $\theta$ 求导，解方程 $\ell&apos;(\theta) = 0$，得到最大似然估计值 $\hat{\theta}_{\text{MLE}}$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查导数符号或二阶导数，验证是否为极大值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&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;ul&gt;
&lt;li&gt;适用范围广，尤其适合已知分布形式但参数未知的情形；&lt;/li&gt;
&lt;li&gt;在样本量足够大时具有良好的统计性质（如一致性、渐近正态性、渐近有效性）；&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;blockquote&gt;
&lt;p&gt;设总体的概率密度 $f(x)=\frac{1}{\theta}e^{-\frac{x-\mu}{\theta}},; x&gt;\mu$，其中 $\mu$ 和 $\theta$ 均未知，求其最大似然估计。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;记样本为 $X_1, X_2, \dots, X_n$，由于样本独立，写出似然函数：
$$
L(\mu, \theta) = \prod_{i=1}^n \frac{1}{\theta} e^{-\frac{X_i - \mu}{\theta}} \cdot I(X_i &gt; \mu)
= \frac{1}{\theta^n} e^{-\frac{1}{\theta} \sum (X_i - \mu)} \cdot I(\mu &amp;#x3C; X_{(1)})
$$
其中 $X_{(1)} = \min(X_1, \dots, X_n)$。&lt;/p&gt;
&lt;p&gt;对数似然函数为：
$$
\ell(\mu, \theta) = -n \log \theta - \frac{1}{\theta} \sum (X_i - \mu), \quad \mu &amp;#x3C; X_{(1)}
$$&lt;/p&gt;
&lt;p&gt;对 $\mu$ 求偏导：
$$
\frac{\partial \ell}{\partial \mu} = -\frac{n}{\theta}&amp;#x3C;0
$$
是单调递减函数，所以 $\ell$ 在 $\mu &amp;#x3C; X_{(1)}$ 区间上最大值取得于 $\mu = X_{(1)}$&lt;/p&gt;
&lt;p&gt;固定 $\mu = X_{(1)}$，再对 $\theta$ 求偏导：&lt;/p&gt;
&lt;p&gt;$$
\ell(\theta) = -n \log \theta - \frac{1}{\theta} \sum (X_i - X_{(1)})
$$&lt;/p&gt;
&lt;p&gt;求导并令其为 0：
$$
\frac{d\ell}{d\theta} = -\frac{n}{\theta} + \frac{1}{\theta^2} \sum (X_i - X_{(1)}) = 0
$$&lt;/p&gt;
&lt;p&gt;解得：
$$
\hat{\theta} = \frac{1}{n} \sum (X_i - X_{(1)})
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终结果：&lt;/strong&gt;
$$
\hat{\mu} = X_{(1)}, \quad
\hat{\theta} = \frac{1}{n} \sum_{i=1}^n (X_i - X_{(1)})
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;估计量的评价&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「无偏性」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;估计量 $\hat{\theta}$ 的期望值等于参数 $\theta$，即 $E(\hat{\theta}) = \theta$。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;样本方差用 $\frac{1}{n-1}$ 是无偏的，而用 $\frac{1}{n}$ 则是有偏的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「一致性」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当样本容量 $n \to \infty$ 时，估计量 $\hat{\theta}$ 收敛到参数 $\theta$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「有效性」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;估计量 $\hat{\theta}$ 的&lt;strong&gt;方差最小&lt;/strong&gt;，即在所有无偏估计量中，$\hat{\theta}$ 的方差最小。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;区间估计&lt;/h2&gt;
&lt;p&gt;在统计推断中，我们常常不仅想得到总体参数的一个&lt;strong&gt;点估计&lt;/strong&gt;，还希望给出一个区间，这个区间能以某种置信程度覆盖真实的参数值，这被称为&lt;strong&gt;区间估计&lt;/strong&gt;（Interval Estimation）。&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;总体 $X$ 有一个参数 $\theta$，样本 $X_1,X_2,\cdots,X_n$，满足：
$$
P{\underline{\theta}(X_1,X_2,\cdots,X_n)&amp;#x3C;\theta&amp;#x3C;\overline{\theta}(X_1,X_2,\cdots,X_n)}\geq 1-\alpha
$$
则随机区间 $(\underline{\theta}, \overline{\theta})$ 称为 $\theta$ 的 $1-\alpha$ 置信区间，上面的大于等于一般只看等号就行了。&lt;/p&gt;
&lt;p&gt;定义 $(\underline{\theta}, \overline{\theta})$ 是&lt;strong&gt;置信区间&lt;/strong&gt;，$1-\alpha$ 是&lt;strong&gt;置信水平&lt;/strong&gt;，$\underline\theta$ 是&lt;strong&gt;置信下限&lt;/strong&gt;，$\overline\theta$ 是&lt;strong&gt;置信上限&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;**置信区间的含义：**每一次试验得到 $x_1,x_2,\cdots,x_n$，对应一个置信区间。反复抽样多次，在这么多次试验中，有 $1-\alpha$ 的比例的置信区间包含了真实的参数 $\theta$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「举例：正态总体 $\mu$ 的置信区间」&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $X \sim N(\mu, \sigma^2)$，样本均值为 $\bar{X}$，方差 $\sigma^2$ 已知，求 $\mu$ 的置信度为 $1-\alpha$ 的置信区间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由题设，根据中心极限定理，样本均值 $\bar{X}$ 的分布为：
$$
\frac{\bar X-\mu}{\sigma/\sqrt{n}}\sim N(0,1)
$$
$$
\Rightarrow P\left{-z_{\alpha/2} &amp;#x3C; \frac{\bar X - \mu}{\sigma/\sqrt{n}} &amp;#x3C; z_{\alpha/2}\right} = 1 - \alpha
$$&lt;/p&gt;
&lt;p&gt;$$
\Rightarrow P\left{\bar X - z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}} &amp;#x3C; \mu &amp;#x3C; \bar X + z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}}\right} = 1 - \alpha
$$&lt;/p&gt;
&lt;p&gt;进而可得：
$$
\mu \in \left[ \bar{X} - z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}},\ \bar{X} + z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}} \right]
$$
其中，$z_{\pm \alpha/2}$ 代表标准正态分布的分位点，需要查表，但已知。&lt;/p&gt;
&lt;p&gt;所以这个区间是仅取决于样本均值 $\bar{X}$ 和已知的总体方差 $\sigma^2$ 的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「枢轴量」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个统计量，不依赖于未知参数。用于构造置信区间。&lt;/p&gt;
&lt;h2&gt;正态总体的区间估计&lt;/h2&gt;
&lt;p&gt;| 待估参数   | 条件            | 枢轴量                                                       | 双侧 $1-\alpha$ 置信区间公式                                 |
| ---------- | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| $\mu$      | $\sigma^2$ 已知 | $\displaystyle Z = \frac{\bar{X} - \mu}{\sigma/\sqrt{n}} \sim N(0,1)$ | $\displaystyle \left[ \bar{X} \pm z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}} \right]$ |
| $\mu$      | $\sigma^2$ 未知 | $\displaystyle T = \frac{\bar{X} - \mu}{S/\sqrt{n}} \sim t(n-1)$ | $\displaystyle \left[ \bar{X} \pm t_{\alpha/2}(n-1) \cdot \frac{s}{\sqrt{n}} \right]$ |
| $\sigma^2$ | $\mu$ 已知      | $\displaystyle \frac{1}{\sigma^2}\sum_{i=1}^n(X_i-\mu)^2 \sim \chi^2(n)$ | $\displaystyle \left[ \frac{\sum\limits_{i=1}^n(X_i-\mu)^2}{\chi^2_{\alpha/2}(n)},\ \frac{\sum\limits_{i=1}^n(X_i-\mu)^2}{\chi^2_{1-\alpha/2}(n)} \right]$ |
| $\sigma^2$ | $\mu$ 未知      | $\displaystyle \frac{(n-1)S^2}{\sigma^2} =\frac{1}{\sigma^2}\sum_{i=1}^n(X_i-\bar X)^2\sim \chi^2(n-1)$ | $\displaystyle \left[ \frac{(n-1)S^2}{\chi^2_{\alpha/2}(n-1)},\frac{(n-1)S^2}{\chi^2_{1-\alpha/2}(n-1)} \right]$ |&lt;/p&gt;
&lt;p&gt;其中 $S^2$ 是样本方差 $\frac{1}{n-1}\sum\limits_{i=1}^n(X_i-\bar X)^2$，$t_{\alpha/2}(n-1)$ 是 $t$ 分布的分位点，$\chi^2_{\alpha/2}(n)$ 是 $\chi^2$ 分布的分位点。&lt;/p&gt;
&lt;p&gt;有些题给定&lt;strong&gt;误差&lt;/strong&gt;，求解样本量 $n$，比如 $\displaystyle \left[ \bar{X} \pm z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}} \right]$ 中，&lt;strong&gt;误差&lt;/strong&gt;是 $z_{\alpha/2} \cdot \frac{\sigma}{\sqrt{n}}$.&lt;/p&gt;</content:encoded></item><item><title>系统级 I/O</title><link>https://www.tonyyin0418.com/blog/csapp/system-io</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/system-io</guid><description>武汉大学2025春季学期 计算机系统基础 第十章 系统级 I/O</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;System-Level I/O&lt;/p&gt;
&lt;p&gt;本章介绍 Unix I/O 和标准 I/O 的一般概念。&lt;/p&gt;
&lt;p&gt;在某些重要情况下，不得不使用 Unix I/O 而不是高级 I/O 函数。&lt;/p&gt;
&lt;h2&gt;Unix I/O&lt;/h2&gt;
&lt;p&gt;以 &lt;code&gt;FILE *fopen&lt;/code&gt; 函数引入，其使用 C 调用了 &lt;code&gt;open&lt;/code&gt;，这是系统调用，是系统级别的 I/O。很多高级 I/O 函数都是用 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt; 和 &lt;code&gt;close&lt;/code&gt; 等系统调用实现的。本节就是介绍这些系统调用。&lt;/p&gt;
&lt;p&gt;Unix I/O 是一种系统调用接口，提供了一组函数来处理对文件和设备的访问，&lt;/p&gt;
&lt;p&gt;Unix I/O 的设计目标是简化 I/O 操作，使其尽可能简单和一致。&lt;/p&gt;
&lt;h3&gt;Overview&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A Linux &lt;strong&gt;「File」&lt;/strong&gt; is a sequence of m bytes.&lt;/li&gt;
&lt;li&gt;所有的 I/O 设备都被视为文件，甚至内核也被视为文件。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat /proc/$$/status&lt;/code&gt; 可以查看当前进程的状态。&lt;/li&gt;
&lt;li&gt;甚至每个进程都有一个 &lt;code&gt;/proc/PID&lt;/code&gt; 目录，里面的 &lt;code&gt;/fd&lt;/code&gt; 是该进程的文件描述符。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内核提供了一系列基础的文件操作：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;open()&lt;/code&gt; and &lt;code&gt;close()&lt;/code&gt; 开关文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read()&lt;/code&gt; and &lt;code&gt;write()&lt;/code&gt; 读写文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stat()&lt;/code&gt;, &lt;code&gt;lstat()&lt;/code&gt;, &lt;code&gt;fstat()&lt;/code&gt; 获取文件的状态信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lseek()&lt;/code&gt; 设置下一次读写的位置，类似访问到哪个字节的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;文件类型&lt;/h3&gt;
&lt;p&gt;类型决定操作方式，包括但不限于以下内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Regular file」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包括文本文件和二进制文件。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「Directory」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Socket」&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title>样本及抽样分布</title><link>https://www.tonyyin0418.com/blog/probability/chap-6</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-6</guid><description>武汉大学2025春季学期 概率论与数理统计 第六章 样本及抽样分布</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&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;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;容量&lt;/strong&gt;是否有限来划分。&lt;/p&gt;
&lt;p&gt;一个总体对应着一个随机变量 $X$，所以对总体的研究就是对随机变量 $X$ 的研究，统称为&lt;strong&gt;总体 $X$&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「样本」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;总体的分布一般是未知的，只知道抽取出的一部分个体的分布，这部分个体称为&lt;strong&gt;样本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;样本也有容量，记为 $n$，样本容量 $n$ 的&lt;strong&gt;样本&lt;/strong&gt;为 $X_1, X_2, \ldots, X_n$ 是随机变量，&lt;strong&gt;样本值（观察值）&lt;/strong&gt; 为 $x_1,x_2\cdots,x_n$.&lt;/p&gt;
&lt;p&gt;样本是从总体中抽取出来的个体的集合，一般是 &lt;strong&gt;简单随机样本&lt;/strong&gt;，用放回抽样的方法得到。当 $N&gt;&gt;n$ 时，不放回抽样也可以近似看作是放回抽样。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「定义」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把样本看成随机向量 $(X1,X2,\cdots,X_n)$，样本值 $(x_1, x_2,\cdots,x_n)$，则&lt;/p&gt;
&lt;p&gt;$(X_1, X_2,\cdots,X_n)$ 的分布函数为：
$$
F^{&lt;em&gt;}(x_1, x_2\cdots, x_n)=\prod_{i=1}^nF(x_i)
$$
概率密度为：
$$
f^{&lt;/em&gt;}(x_1, x_2\cdots, x_n)=\prod_{i=1}^nf(x_i)
$$&lt;/p&gt;
&lt;h2&gt;直方图和箱线图&lt;/h2&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;/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;blockquote&gt;
&lt;p&gt;样本分位数：设有容量为 $n$ 的样本观察值 $x1,x2,\cdots,x_n$，样本 $p$ 分位数 $0&amp;#x3C;p&amp;#x3C;1$ 记为 $x_p$，则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;至少有 $np$ 个观察值小于或等于 $x_p$，&lt;/li&gt;
&lt;li&gt;至少有 $n(1-p)$ 个观察值大于或等于 $x_p$.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;画箱线图要关注以下五个数值：&lt;/p&gt;
&lt;p&gt;最小值 $\text{Min}$，第一四分位数 $Q_1$，中位数 $M$，第三四分位数 $Q_3$，最大值 $\text{Max}$。&lt;/p&gt;
&lt;p&gt;箱线图的绘制步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算样本的五个数值 $\text{Min}, Q_1, M, Q_3, \text{Max}$.&lt;/li&gt;
&lt;li&gt;在横轴上标出这五个数值。&lt;/li&gt;
&lt;li&gt;在横轴上画出一个矩形，矩形的左边界为 $Q_1$，右边界为 $Q_3$，矩形的中间线为 $M$。&lt;/li&gt;
&lt;li&gt;在矩形的左边画一条线段，连接 $\text{Min}$ 和 $Q_1$，在矩形的右边画一条线段，连接 $Q_3$ 和 $\text{Max}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以反应以下性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中心位置&lt;/li&gt;
&lt;li&gt;散布程度：通过 $Q_1$ 和 $Q_3$ 的距离（四分位距）来衡量。&lt;/li&gt;
&lt;li&gt;对称性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于离群点的量化判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $x &amp;#x3C; Q_1 - 1.5 \times IQR$ 或 $x &gt; Q_3 + 1.5 \times IQR$，则认为 $x$ 是离群点&lt;/li&gt;
&lt;li&gt;其中 $IQR = Q_3 - Q_1$，是 Interquartile Range，四分位距。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&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;其定义为：$F_n(x)$ 表示小于等于 $x$ 的观察值所占的比率。&lt;/p&gt;
&lt;p&gt;即：
$$
F_n(x)=\frac{(x_1,x_2,\cdots,x_n)\text{ 中小于等于 }x\text{ 的个数} }{n}
$$&lt;/p&gt;
&lt;p&gt;呈现出来的就是一个，每次跃升的高度为 $\frac{1}{n}$ 的阶梯函数。&lt;/p&gt;
&lt;p&gt;当 $n\to +\infty$ 时，$F_n(x)$ 可以当做 $F(x)$ 来使用，这是&lt;strong&gt;格里汶科定理&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;样本统计量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;统计量：&lt;/strong&gt; $g(X_1,X_2\cdots,X_n)$ 是样本 $(X_1,X_2\cdots,X_n)$ 的&lt;strong&gt;函数&lt;/strong&gt;，不含任何未知参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统计值：&lt;/strong&gt;$g(x_1,x_2,\cdots,x_n)$ 是 $g(X_1,X_2,\cdots,X_n)$ 的观测值。&lt;/p&gt;
&lt;p&gt;思路是要模仿总体中的统计量，&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「样本均值」&lt;/strong&gt;
$$
\bar{X} = \frac{1}{n}\sum_{i=1}^n X_i
$$
&lt;strong&gt;「样本方差」&lt;/strong&gt;
$$
S^2 = \frac{1}{n-1}\sum_{i=1}^n (X_i - \bar{X})^2=\frac{1}{n-1}(\sum_{i=1}^nX_i^2-n\bar X^2)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;除以 $n-1$ 这是为了保证样本方差的无偏性，需要记住。中间是定义式，展开即可右侧，是&lt;strong&gt;计算中常用的形式&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;「样本标准差」&lt;/strong&gt;
$$
S = \sqrt{S^2} = \sqrt{\frac{1}{n-1}\sum_{i=1}^n (X_i - \bar{X})^2}
$$
&lt;strong&gt;「样本 $k$ 阶原点矩」&lt;/strong&gt;
$$
A_k = \frac{1}{n}\sum_{i=1}^n X_i^k
$$
&lt;strong&gt;「样本 $k$ 阶中心矩」&lt;/strong&gt;
$$
B_k = \frac{1}{n}\sum_{i=1}^n (X_i - \bar{X})^k
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对应的总体概念，总体 $k$ 阶原点矩：$E[X^k]$，总体 $k$ 阶中心矩：$E[(X-EX)^k]$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;统计量的数字特征&lt;/h2&gt;
&lt;p&gt;设总体 $X$ 无论服从什么分布，满足期望 $EX=\mu$ 和方差 $DX=\sigma^2$，则：
$$
E(X_i)=\mu,\quad D(X_i)=\sigma^2
$$
这是很容易得到的。进而有&lt;strong&gt;三个重要公式&lt;/strong&gt;：
$$
\color{red}{E(\bar X)=\mu,\quad D(\bar X)=\frac{\sigma ^2}{n},\quad E(S^2)=\sigma^2}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$$
E(\bar X)=E(\frac{1}{n}\sum_{i=1}^nX_i)=\frac{1}{n}\sum_{i=1}^nEX_i=\mu
$$&lt;/p&gt;
&lt;p&gt;$$
D(\bar X)=D(\frac{1}{n}\sum_{i=1}^nX_i)=\frac{1}{n^2}\sum_{i=1}^nEX_i=\frac{\sigma^2}{n}
$$&lt;/p&gt;
&lt;p&gt;$$
E(S^2)=E\left[\frac{1}{n-1}(\sum_{i=1}^nX_i^2-n\bar X^2)\right]=\frac{1}{n-1}\left(\sum_{i=1}^nE(X_i^2)-nE(\bar X^2)\right)
$$&lt;/p&gt;
&lt;p&gt;$$
\Rightarrow=E(S^2)=\frac{1}{n-1}\left[n(\mu^2+\sigma^2)-n(\mu^2+\frac{\sigma^2}{n})\right]=\sigma^2
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;利用这三个公式，可以做很多变形，从而解题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;抽样分布&lt;/h2&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_1,X_2,\cdots,X_n$ 来自总体 $N(0,1)$ 且相互独立，则随机变量
$$
\chi^2=X_1^2+X_2^2+\cdots+X_n^2,\quad X_i\sim N(0,1)
$$
服从&lt;strong&gt;自由度&lt;/strong&gt;为 $n$ 的卡方分布，记为 $\chi^2\sim \chi^2(n)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自由度越高，分布越接近正态分布&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里并不重要。卡方分布的概率密度公式为
$$
f(y)=\left{
\begin{aligned}
\dfrac{1}{2^{n/2}\Gamma(n/2)}y^{n/2-1}e^{-y/2}, &amp;#x26;y&gt;0\
0, &amp;#x26;\text{Otherwise}
\end{aligned}
\right.
$$
其中 $\Gamma$ 是一种分布函数。
&lt;strong&gt;「可加性」&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设 $\chi^2_1=\chi^2(n_1)$，$\chi_2^2=\chi^2(n_2)$ ，且 $\chi_1^2$ 和 $\chi_2^2$ 相互独立，则：
$$
\chi_1^2+\chi_2^2=\chi^2(n1+n2)
$$
&lt;strong&gt;「期望和方差」&lt;/strong&gt;
$$
E(\chi^2_n)=n,\quad D(\chi^2_n)=2n
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分位点」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设分布函数 $F(x)$，若有：
$$
P{X&gt;x_\alpha}=\alpha
$$
则称 $x_\alpha$ 是 $F(x)$ 的上 $\alpha$ 分位点。&lt;/p&gt;
&lt;p&gt;也就是说，$x_\alpha$ 是一个横坐标，纵坐标是概率密度，右侧的面积是概率，概率等于 $\alpha$.&lt;/p&gt;
&lt;h3&gt;t 分布&lt;/h3&gt;
&lt;p&gt;设 $X\sim N(0,1),Y\sim \chi^2(n)$，且 $X,Y$ 独立，则：
$$
t=\frac{X}{\sqrt{Y/n}}\sim t(n)
$$
服从自由度为 $n$ 的 &lt;strong&gt;t 分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当 $n\to \infty$ 时，t 分布近似于 $N(0,1)$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分位点」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;概率密度是偶函数，所以分位点有：$t_{1-\alpha}(n)=-t_{\alpha}(n)$.&lt;/p&gt;
&lt;h3&gt;F 分布&lt;/h3&gt;
&lt;p&gt;设 $X\sim \chi^2(n_1)$，$Y\sim \chi^2(n_2)$，且 $X,Y$ 独立，则：
$$
F=\frac{X/n1}{Y/n2}\sim F(n_1,n_2)
$$
服从自由度为 $(n_1,n_2)$ 的 &lt;strong&gt;F 分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「与 t 分布的关系」&lt;/strong&gt;
$$
X\sim t(n)\Rightarrow X^2\sim F(1,n)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是因为：$X=\frac{U}{\sqrt{V/n}}\Rightarrow X^2=\frac{U^2/1}{V/n}$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;「分位点」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$F_{1-\alpha}(n_1,n_2)=\dfrac{1}{F_\alpha(n_2,n_1)}$&lt;/p&gt;
&lt;h3&gt;特征总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$\chi^2(n)$：平方和；&lt;/li&gt;
&lt;li&gt;$t(n)$：正态 / 根号下平方和，或分母有绝对值。&lt;/li&gt;
&lt;li&gt;$F(n_1,n_2)$：平方和 / 平方和。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $X_1,X_2,\cdots,X_5$ 为总体 $X\sim N(0,1)$ 的一个样本，设 $Y=(X_1+X_2+X_3)^2+(X_4-\sqrt{2}X_5)^2$ 且 $cY$ 服从 $\chi^2$ 分布，求系数 $c$ 的值.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;把每个被平方的项，化为标准正态分布。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$X_1+X_2+X_3\sim N(0,3)$，标准化，则有：
$$
A=\frac{X_1+X_2+X_3-0}{\sqrt{3}}\sim N(0,1)
$$
$X_4-\sqrt{2}X_5\in N(0,3)$，标准化，则有：
$$
B=\frac{X_4-\sqrt{2}X_5-0}{\sqrt{3}}\sim N(0,1)
$$
另外 $A,B$ 显然独立，故 $A^2+B^2=\dfrac{1}{3}Y\sim \chi^2(2)$，$c=\dfrac{1}{3}$.&lt;/p&gt;
&lt;h2&gt;正态总体抽样分布&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;$\dfrac{(\bar X-\mu)}{\sigma/\sqrt{n}}\sim N(0,1)$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;样本均值 $\bar X$ 和样本方差 $S^2$ 相互独立&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\frac{(n-1)S^2}{\sigma^2}=\frac{\sum\limits_{i=1}^n(X_i-\bar X)^2}{\sigma^2}\sim\chi^2(n-1)
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{\sum\limits_{i=1}^n(X_i-\mu)^2}{\sigma^2}\sim\chi^2(n)
$$&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;$T=\dfrac{\bar X-\mu}{S/\sqrt{n}}\sim t(n-1)$&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title>大数定律及中心极限定理</title><link>https://www.tonyyin0418.com/blog/probability/chap-5</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-5</guid><description>武汉大学2025春季学期 概率论与数理统计 第五章 大数定律及中心极限定理</description><pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&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;&lt;strong&gt;「弱大数定律」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $X_1, X_2, \ldots, X_n$ 是独立同分布的随机变量，数学期望 $E(X_k)=\mu$，则对于任意 $\epsilon&gt;0$，有：
$$
P\left(\left|\frac{1}{n}\sum_{k=1}^n X_k - \mu\right|&gt;\epsilon\right) \to 0 \quad (n \to \infty)
$$&lt;/p&gt;
&lt;p&gt;通俗的说，就是当样本容量 $n$ 趋近于无穷大时，样本均值 $\frac{1}{n}\sum_{k=1}^n X_k$ 会以概率收敛到总体均值 $\mu$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「伯努利大数定律」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $f_A$ 是 $n$ 次独立重复试验中事件 $A$ 发生的频率，$p$ 是事件 $A$ 的概率，则对于任意 $\epsilon&gt;0$，有：
$$
P\left(\left|\frac{f_A}{n} - p\right|&amp;#x3C;\epsilon\right) \to 1 \quad (n \to \infty)
$$&lt;/p&gt;
&lt;h2&gt;中心极限定理&lt;/h2&gt;
&lt;p&gt;大数定律关注的是：$\bar{X}_n$ 最终会接近 $\mu$，而中心极限定理要解决的是：$\bar{X}_n$ 附近是怎样波动的？&lt;/p&gt;
&lt;p&gt;简单来说，结论为：&lt;strong&gt;不管原始变量是什么分布，平均值的分布都会近似正态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;预先定义：对于随机变量 $X$ 有均值 $\mu$ 和方差 $\sigma^2$，则&lt;strong&gt;标准化变量&lt;/strong&gt; $Z=\dfrac{X-\mu}{\sigma}$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「独立同分布的中心极限定理」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设随机变量 $X_1, X_2, \ldots, X_n$ 独立同分布，数学期望 $E(X_k)=\mu$，方差 $D(X_k)=\sigma^2$，&lt;/p&gt;
&lt;p&gt;则当 $n \to \infty$ 时：标准化变量满足
$$
Z_n=\frac{\sqrt{n}(\bar{X}_n - \mu)}{\sigma}=\frac{\sum X_n-n\cdot \mu}{\sqrt{n}\cdot \sigma}\sim N(0, 1)
$$
或改写为：
$$
\bar{X}_n\sim N(\mu, \frac{\sigma^2}{n})
$$
也就是说，算术平均值 $\bar{X}_n$，在 $n$ 充分大时，服从均值为 $\mu$，方差为 $\frac{\sigma^2}{n}$ 的正态分布。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Lyapunov 定理」&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;设 $X_1, X_2, \ldots, X_n$ 独立同分布，数学期望 $E(X_k)=\mu_k$，方差 $D(X_k)=\sigma_k^2$，总方差为 $s_n^2=\sum_{k=1}^n \sigma_k^2$，&lt;/p&gt;
&lt;p&gt;若存在 $\delta&gt;0$ 使得当 $n \to \infty$ 时：
$$
\lim_{n \to \infty} \frac{1}{s_n^{2 + \delta}} \sum_{i=1}^n E\left[ |X_i - \mu_i|^{2 + \delta} \right] \to 0
$$
则结论与独立同分布的中心极限定理相同：标准化变量满足&lt;/p&gt;
&lt;p&gt;$$
Z_n=\frac{\sum\limits_{k=1}^{n}X_k-\sum\limits_{k=1}^{n}\mu_k}{s_n} \sim N(0, 1)
$$&lt;/p&gt;
&lt;h2&gt;例题&lt;/h2&gt;
&lt;p&gt;例题中常用的主要是第一个定理，独立同分布的中心极限定理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;计算器在进行加法时，将每个加数舍入最靠近它的整数，设所有舍入误差相互独立且在 $(-0.5,0.5)$ 上服从均匀分布。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 $1500$ 个数相加，问误差综合的绝对值超过 $15$ 的概率是多少？&lt;/li&gt;
&lt;li&gt;最多可有几个数相加使得误差总和的绝对值小于 $10$ 的概率不小于 $0.90$.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;$X_k$ 表示第 $k$ 个数的误差，$X_k \sim U(-0.5, 0.5)$，则 $E(X_k)=0$，$D(X_k)=\frac{1}{12}$.&lt;/p&gt;
&lt;p&gt;设 $X=\sum_{k=1}^{1500}X_k$，则根据中心极限法则，
$$
Z=\frac{X-1500\times 0}{\sqrt{1500 \cdot \frac{1}{12}}} \sim N(0, 1)
$$&lt;/p&gt;
&lt;p&gt;所以概率：
$$
P(|X|&gt;15)=1-P\left(\left|Z\right|\leq\frac{15}{\sqrt{1500 \cdot \frac{1}{12}}}\right)=1-\left[2\Phi(\frac{15}{\sqrt{1500 \cdot \frac{1}{12}}})-1\right]=0.1802
$$&lt;/p&gt;</content:encoded></item><item><title>随机变量的数字特征</title><link>https://www.tonyyin0418.com/blog/probability/chap-4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-4</guid><description>武汉大学2025春季学期 概率论与数理统计 第四章 随机变量的数字特征</description><pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第四章：随机变量的数字特征&lt;/p&gt;
&lt;p&gt;本章主要讨论描述随机变量某种特征的常数。&lt;/p&gt;
&lt;h2&gt;数学期望&lt;/h2&gt;
&lt;p&gt;设离散型随机变量 $X$ 的取值为 $x_1,x_2,\ldots,x_n$，对应的概率为 $p_1,p_2,\ldots,p_n$，则称随机变量 $X$ 的数学期望为：
$$
E(X) = \sum_{k=1}^{\infty}x_k \cdot p_k
$$
对于连续型随机变量，类似有：
$$
E(X)=\int_{-\infty}^{\infty}xf(x)\text{d}x
$$
简称期望。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「随机变量的函数的期望」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $X$ 是离散型随机变量，$Y=g(x)$ 是 $X$ 的函数，分布律满足 $P(X=x_k)=p_k$，则 $Y$ 的期望为：
$$
E(Y)=E[g(X)]=\sum_{k=1}^{\infty}g(x_k)\cdot p_k
$$
连续型类似有：
$$
E(Y)=E[g(X)]=\int_{-\infty}^{\infty}g(x)f(x)\text{d}x
$$
所以，&lt;strong&gt;我们在求 $E(Y)$ 时，不必算出 Y 的分布律或概率密度，只需使用 X 的分布律或概率密度。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「性质」&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若 $C$ 是常数，$E(C)=C$.&lt;/li&gt;
&lt;li&gt;若 $C$ 是常数，$E(CX)=CE(X)$.&lt;/li&gt;
&lt;li&gt;$E(X+Y)=E(X)+E(Y)$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;若 $X, Y$ 相互独立&lt;/strong&gt;，$E(XY)=E(X)E(Y)$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;方差&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「定义」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $X$ 是随机变量，&lt;strong&gt;方差&lt;/strong&gt;：
$$
D(X)=\text{Var}(X)=E{[X-E(X)]^2}
$$
而&lt;strong&gt;标准差为&lt;/strong&gt;
$$
\sigma_X = \sqrt{D(X)} = \sqrt{\mathrm{Var}(X)}
$$
把期望的公式代入，对于离散型随机变量：
$$
D(X)=\sum_{k=1}^{\infty}[x_k-E(X)]^2p_k
$$
连续型随机变量：
$$
D(X)=\int_{-\infty}^{\infty}[x-E(X)]^2f(x)\text{d}x
$$
&lt;strong&gt;「期望和方差的关系」&lt;/strong&gt;
$$
D(X)=E(X^2)-[E(X)]^2
$$
&lt;strong&gt;「切比雪夫不等式」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设随机变量 $X$ 具有数学期望 $E(X)=\mu$ 和方差 $D(X)=\sigma^2$，则对任意正数 $\epsilon$，都有：
$$
P{|X-\mu|\geq \epsilon}\leq \frac{\sigma^2}{\epsilon^2}
$$
用于在不知道分布，只知道方差和期望时，估计这种概率的界限，但是&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;ol&gt;
&lt;li&gt;
&lt;p&gt;若 $C$ 是常数，$D(C)=0$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若 $C$ 是常数，$D(CX)=C^2\cdot D(X)$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$D(X+Y)=D(X)+D(Y)+2E{[X-E(X)][Y-E(Y)]}$，&lt;/p&gt;
&lt;p&gt;当 $X, Y$ 独立时，有 $D(X+Y)=D(X)+D(Y)$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$D(X)=0 \Longleftrightarrow P{X=E(X)}=1$.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;协方差和相关系数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「定义」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $X$ 和 $Y$ 是两个随机变量，&lt;strong&gt;协方差&lt;/strong&gt;：
$$
\text{Cov}(X, Y)=E{[X-E(X)][Y-E(Y)]}
$$
而&lt;strong&gt;相关系数为&lt;/strong&gt;
$$
\rho_{XY}=\frac{\text{Cov}(X, Y)}{\sqrt{D(X)}\sqrt{D(Y)}}
$$
$\rho_{XY}$ 衡量 $X$ 与 $Y$ 的线性相关程度，范围为 $[-1, 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;ol&gt;
&lt;li&gt;$\text{Cov}(X, Y)=\text{Cov}(Y, X)$.&lt;/li&gt;
&lt;li&gt;$\text{Cov}(aX, bY)=ab\cdot \text{Cov}(X, Y)$.&lt;/li&gt;
&lt;li&gt;$\text{Cov}(X+Y, Z)=\text{Cov}(X, Z)+\text{Cov}(Y, Z)$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;相关系数&lt;/strong&gt;是在协方差基础上进行&lt;strong&gt;标准化&lt;/strong&gt;，反映两个变量之间的&lt;strong&gt;线性关系强弱和方向&lt;/strong&gt;，数值范围在 -1 到 1 之间。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$|\rho_{XY}|\leq 1$.&lt;/li&gt;
&lt;li&gt;$|\rho_{XY}=1|\Longleftrightarrow$ 存在常数 $a, b$ 使 $P{Y=a+bX}=1$.&lt;/li&gt;
&lt;li&gt;当 $\rho_{XY}=0$ 时，$X$ 和 $Y$ &lt;strong&gt;不相关&lt;/strong&gt;，但不一定独立。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;「与二维正态分布」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从正态密度函数出发，二维正态分布的密度为：
$$
f(x, y) = \frac{1}{2\pi \sigma_X \sigma_Y \sqrt{1-\rho^2}} \exp\left( -\frac{1}{2(1-\rho^2)} Q(x, y) \right)
$$
其中 $Q(x, y)$ 是一个二次型：
$$
Q(x, y) = \left( \frac{(x - \mu_X)^2}{\sigma_X^2} - 2\rho \frac{(x - \mu_X)(y - \mu_Y)}{\sigma_X \sigma_Y} + \frac{(y - \mu_Y)^2}{\sigma_Y^2} \right)
$$
这里面的&lt;strong&gt;常数 $\rho$ 就是相关系数&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;常见分布&lt;/h2&gt;
&lt;p&gt;离散型分布：&lt;/p&gt;
&lt;p&gt;| 分布名称   | 参数                       | 分布律                                               | 期望 $E[X]$   | 方差 $Var(X)$       | 记号                        |
| ---------- | -------------------------- | ---------------------------------------------------- | ------------- | ------------------- | --------------------------- |
| 伯努利分布 | $p$    | $P(X = 1) = p$$P(X = 0) = 1 - p$ | $p$           | $p(1 - p)$          | $\mathrm{Bern}(p)$          |
| 二项分布   | $n$，$p$ | $P(X = k) = \binom{n}{k} p^k (1 - p)^{n - k}$        | $np$          | $np(1 - p)$         | $\mathrm{Bin}(n, p)$        |
| 几何分布   | $p$               | $P(X = k) = (1 - p)^{k - 1} p$ | $\dfrac{1}{p}$ | $\dfrac{1 - p}{p^2}$ | $\mathrm{Geom}(p)$          |
| 泊松分布   | $\lambda &gt; 0$   | $P(X = k) = \dfrac{\lambda^k e^{-\lambda}}{k!}$      | $\lambda$     | $\lambda$           | $\mathrm{Poisson}(\lambda)$ |
| 超几何分布   | $N,M,n$       | $P(X = k) = \dfrac{\binom{M}{k} \binom{N - M}{n - k}}{\binom{N}{n}}$                                      | $n \dfrac{M}{N}$     | $n\dfrac{M}{N} \cdot (1-\dfrac{M}{N}) \cdot \dfrac{N - n}{N - 1}$ | $\mathrm{H}(N, M, n)$     |&lt;/p&gt;
&lt;p&gt;连续型分布：&lt;/p&gt;
&lt;p&gt;| 分布名称 | 参数            | 概率密度函数                                                 | 期望 $E[X]$         | 方差 $Var(X)$          | 记号                         |
| -------- | --------------- | ------------------------------------------------------------ | ------------------- | ---------------------- | ---------------------------- |
| 均匀分布 | $[a, b]$        | $f(x) = \frac{1}{b - a},\quad x \in [a, b]$                  | $\frac{a + b}{2}$   | $\frac{(b - a)^2}{12}$ | $\mathrm{U}(a, b)$           |
| 指数分布 | $\lambda &gt; 0$   | $f(x) = \lambda e^{-\lambda x},\quad x \ge 0$                | $\frac{1}{\lambda}$ | $\frac{1}{\lambda^2}$  | $\mathrm{Exp}(\lambda)$      |
| 正态分布 | $\mu, \sigma^2$ | $f(x) = \frac{1}{\sqrt{2\pi \sigma^2}} \exp\left( -\frac{(x - \mu)^2}{2\sigma^2} \right)$ | $\mu$               | $\sigma^2$             | $\mathcal{N}(\mu, \sigma^2)$ |&lt;/p&gt;
&lt;h2&gt;例题&lt;/h2&gt;
&lt;p&gt;若 $Y = aX + b$，则：
$$
E(Y) = aE(X) + b,\quad D(Y) = a^2D(X)
$$&lt;/p&gt;</content:encoded></item><item><title>异常控制流 I</title><link>https://www.tonyyin0418.com/blog/csapp/ecf-procs1</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/ecf-procs1</guid><description>武汉大学2025春季学期 计算机系统基础 第八章 异常控制流 I</description><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Exceptional Control Flow: Exceptions and Processes&lt;/p&gt;
&lt;p&gt;在之前的学习主要关注静态代码，直到加载程序。而本章开始关注动态的过程，讨论程序执行的具体动态过程。&lt;/p&gt;
&lt;p&gt;这章对应的 Lab 是 Shell Lab.&lt;/p&gt;
&lt;h2&gt;Exceptional Control Flow&lt;/h2&gt;
&lt;p&gt;正常情况下，程序的控制流是线性的，顺序执行。CPU 朴素地从指令序列上读取并依次执行。&lt;/p&gt;
&lt;p&gt;而我们知道，在实际的程序中，控制流并不是线性的，称为异常控制流，分为两个层面的状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序状态 Program state
&lt;ul&gt;
&lt;li&gt;条件跳转 Jumps and branches&lt;/li&gt;
&lt;li&gt;函数调用 Call and return&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统状态 System state
&lt;ul&gt;
&lt;li&gt;比如键盘按 &lt;code&gt;Ctrl-C&lt;/code&gt;，除零等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在实际的程序中，控制流的改变是非常复杂的，统称为 &lt;strong&gt;Exceptional Control Flow 异常控制流&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;不同层级上的机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Low level mechanisms：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Exceptions 异常」&lt;/strong&gt; 系统层面，需要硬件和操作系统一起解决。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Higher level machinisms：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Process context switch 进程切换」&lt;/strong&gt; 需要操作系统 + 硬件计时器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Signals 信号」&lt;/strong&gt; 需要操作系统；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Nonlocal jumps 非本地跳转」&lt;/strong&gt; 需要 C runtime library 实现，如 &lt;em&gt;&lt;code&gt;setjmp&lt;/code&gt;&lt;/em&gt; 和 &lt;em&gt;&lt;code&gt;longjmp&lt;/code&gt;&lt;/em&gt; 函数配合使用，实现局部函数调用栈以外的异常跳转，属于用户级控制流转变机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exceptions&lt;/h2&gt;
&lt;p&gt;这是异常控制流的最底层机制，异常是由 CPU 触发的，通常是由于硬件错误或者系统调用引起的。&lt;/p&gt;
&lt;p&gt;对于不同的异常，有一个独立的异常号，与对应的异常处理程序的地址一起，对应存储在 &lt;strong&gt;「Exception Tables」&lt;/strong&gt; 中，非常类似于跳转表。这个表的起始地址会存储在一个 CPU 的特殊寄存器中。但是好像在不同的系统中有不同实现。&lt;/p&gt;
&lt;p&gt;系统在发现异常后，会根据 Exception Tables，转到操作系统中对应的异常处理程序，并执行不同操作。&lt;/p&gt;
&lt;p&gt;异常有很多种类型，下面介绍一些类型的异常及处理程序。&lt;/p&gt;
&lt;h3&gt;Interrupts&lt;/h3&gt;
&lt;p&gt;中断，Asynchronous Exceptions.&lt;/p&gt;
&lt;p&gt;由处理器连接的外部设备引起的异常，比如键盘、鼠标、网络等。&lt;/p&gt;
&lt;p&gt;CPU 会在当前指令完成后，再处理外部异常，之后回到原来的指令的下一条继续执行。&lt;/p&gt;
&lt;h3&gt;Synchronous Exceptions&lt;/h3&gt;
&lt;p&gt;简称异常，是 CPU 内部的异常，细分为以下类别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traps&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Intentional，由程序员主动引起的异常；&lt;/li&gt;
&lt;li&gt;比如 &lt;strong&gt;system calls&lt;/strong&gt;，breakpoint 等等。&lt;/li&gt;
&lt;li&gt;可以在处理结束后，从原来的下一条指令继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faults&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Unintentional，部分 recoverable ；&lt;/li&gt;
&lt;li&gt;这里 recoverable 的含义是：&lt;strong&gt;内核&lt;/strong&gt; 能否让同一指令重新尝试，也就是有可能可以通过重复程序恢复的。&lt;/li&gt;
&lt;li&gt;比如 page faults (recoverable)，protection faults (unrecoverable)，除零错误 (unrecoverable)；&lt;/li&gt;
&lt;li&gt;对于 recoverable 的错误，回到原指令重试；对于 unrecoverable 的，中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aborts&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Unintentional and unrecoverable；&lt;/li&gt;
&lt;li&gt;致命错误，不返回程序，直接中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Processes&lt;/h2&gt;
&lt;p&gt;进程是一个正在执行的程序的实例（Instance），也是 The most profound ideas in computer science.&lt;/p&gt;
&lt;p&gt;这里，我们不关注操作系统如何实现进程，而是关注进程对于程序执行的影响，它提供独立的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Logical control flow」&lt;/strong&gt; 独立的控制流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Private address space」&lt;/strong&gt; 隔离开的内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让程序有假象，好像其正在独占 CPU 和内存。&lt;/p&gt;
&lt;p&gt;下面具体展开上述的两个方面。&lt;/p&gt;
&lt;h3&gt;Logical Control Flow&lt;/h3&gt;
&lt;p&gt;先考虑最简单的情况，在只有一个核心的 CPU 上，只能物理上同时运行一个进程。然而系统肯定要同时运行多个进程，这就需要进行并发执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「并发流 Concurrent flow」&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;多个进程/线程在时间上交替执行，CPU 在它们之间快速切换，形成错觉；&lt;/li&gt;
&lt;li&gt;每个进程分配的 &lt;strong&gt;time slice&lt;/strong&gt; 很短。&lt;/li&gt;
&lt;li&gt;物理上某时刻只有一个进程在运行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrent 的定义：&lt;/strong&gt; 称两个任务并发，当且仅当他们的执行时间有重叠；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「并行流 Parallel flow」&lt;/strong&gt;：在多核CPU上，不同核上的进程/线程可以真正同时运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./ecf-procs1-01.jpg&quot; alt=&quot;Concurrent Processes&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Context Switching&lt;/h3&gt;
&lt;p&gt;操作系统利用&lt;strong&gt;上下文切换&lt;/strong&gt;来实现多任务。&lt;/p&gt;
&lt;p&gt;上下文会存储一个进程的状态，用来恢复它，一般包括：寄存器、程序寄存器、用户栈等各种各样信息。&lt;/p&gt;
&lt;p&gt;上下文切换时，CPU需要在&lt;strong&gt;内核模式&lt;/strong&gt;下执行调度代码，恢复目标进程上下文后，切换到该进程的&lt;strong&gt;用户模式&lt;/strong&gt;运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User mode&lt;/strong&gt;：受限状态，不能直接访问硬件或其它进程的内存；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel Mode&lt;/strong&gt;：最高特权，可以访问任何资源，执行任何指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./ecf-procs1-02.jpg&quot; alt=&quot;Context Switching&quot;&gt;&lt;/p&gt;
&lt;h2&gt;System Calls&lt;/h2&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;调用的指令不同，&lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;syscall&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;System Call 用 &lt;strong&gt;内核&lt;/strong&gt; 执行，而Function Call 用 &lt;strong&gt;应用程序&lt;/strong&gt; 执行。&lt;/li&gt;
&lt;li&gt;Different set of privileges，系统调用执行在内核态（Kernel mode），而普通函数调用在用户态（User mode）。系统调用通过硬件特权级切换允许程序访问受保护资源，确保系统安全性和稳定性。&lt;/li&gt;
&lt;li&gt;有变量 &lt;code&gt;errno&lt;/code&gt; 表示系统调用的错误类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体的流程是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 System Call 的编号存到 &lt;code&gt;%rax&lt;/code&gt; 中&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;syscall&lt;/code&gt; 指令，操作系统就会自动切换到相应处理程序，用内核执行。&lt;/li&gt;
&lt;li&gt;注意这里 &lt;code&gt;syscall&lt;/code&gt; 是一条汇编层面的机器指令，后面也不用跟任何参数。但是不代表不需要参数，如果这个系统调用需要传参，也需要放到 &lt;code&gt;%rdi,%rsi&lt;/code&gt; 等寄存器中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./ecf-procs1-03.jpg&quot; alt=&quot;System calls&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Process Control&lt;/h2&gt;
&lt;p&gt;进程控制是操作系统提供的 API，允许用户程序创建、终止和管理进程。&lt;/p&gt;
&lt;p&gt;每个进程都有非零 PID：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getpid()&lt;/code&gt;：获取当前进程的 ID；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getppid()&lt;/code&gt;：获取当前进程的父进程 ID；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个 process 可以分为以下四种状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Running」&lt;/strong&gt; 正在运行，或者正在等待 CPU；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Blocked / Sleeping」&lt;/strong&gt; 等待某个外部事件的发生；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Stopped」&lt;/strong&gt;：被用户暂停 &lt;code&gt;Ctrl+Z&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Terminated / Zombie」&lt;/strong&gt;：进程已经结束，但是还没有被父进程回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Terminate&lt;/h3&gt;
&lt;p&gt;进程的终止：
$$
\mathtt{void};; \mathtt{exit(int;\ status)}
$$
让进程以指定的状态码退出。&lt;/p&gt;
&lt;p&gt;这个函数 &lt;strong&gt;调用一次，不返回&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Fork&lt;/h3&gt;
&lt;p&gt;父进程创建子进程，子进程是父进程的副本，拥有独立的地址空间。&lt;/p&gt;
&lt;p&gt;子进程与父进程的比较：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行相关的信息（如寄存器、栈）相同；&lt;/li&gt;
&lt;li&gt;PID 不同；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathtt{int}\quad \mathtt{fork(void)}
$$&lt;/p&gt;
&lt;p&gt;是 C 语言中的一个系统调用，创建一个新的进程，返回值为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给父进程返回子进程的 PID；&lt;/li&gt;
&lt;li&gt;给子进程返回 0；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个函数&lt;strong&gt;调用一次，返回两次&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;事实上，系统会采用 Copy-on-write 的方式来实现 fork，父进程和子进程共享同一块内存区域，直到其中一个进程修改该内存区域时，操作系统才会为其分配新的内存空间，以避免不必要的内存复制和开销。&lt;/p&gt;
&lt;h3&gt;Reaping Child Processes&lt;/h3&gt;
&lt;p&gt;一个进程终止后，会变成 &lt;strong&gt;「Zombie」&lt;/strong&gt; 状态的进程，这是一种半死不活的状态，虽然运行结束了，但是占有了资源，没有被回收。因此，子进程在终止后，父进程需要负责回收。&lt;/p&gt;
&lt;p&gt;如果父进程比子进程更早终止了，这时子进程称为 &lt;strong&gt;「Orphaned child」&lt;/strong&gt;，会被操作系统收养，成为 &lt;code&gt;init&lt;/code&gt; 进程的子进程。这是一个 $\text{PID}=1$ 的特殊进程，负责收养所有的孤儿进程。&lt;/p&gt;
&lt;p&gt;需要注意的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只有已经结束了的子进程才会被回收&lt;/strong&gt;，Running 的子进程不会被 Terminate。&lt;/li&gt;
&lt;li&gt;回收子进程&lt;strong&gt;没有特定顺序&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reaping 回收 zombie 进程，依靠的是调用 &lt;code&gt;wait()&lt;/code&gt; 或 &lt;code&gt;waitpit()&lt;/code&gt;来等待子进程结束：
$$
\mathtt{pid_t}\quad \mathtt{waitpid(pid_t;;pid,int;*statusp,int;;options);}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pid&lt;/code&gt; 是进程 ID，也表示等待的子进程集合，比如当 &lt;code&gt;pid&lt;/code&gt; 为 -1 时，表示等待所有的子进程结束。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;statusp&lt;/code&gt; 是指向&lt;strong&gt;子进程的退出状态&lt;/strong&gt; &lt;code&gt;status&lt;/code&gt; 的指针，可以查表得知退出的具体状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;options&lt;/code&gt; 是一些其他选项。&lt;/li&gt;
&lt;li&gt;如果等待集合中，有一个子进程结束了，函数就会返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathtt{pid_t}\quad \mathtt{wait(int;;*statusp)}
$$&lt;/p&gt;
&lt;p&gt;函数是 &lt;code&gt;waitpid&lt;/code&gt; 的简化版本，等价于 $\mathtt{waitpid(-1,&amp;#x26;statu,0)}$.&lt;/p&gt;
&lt;h3&gt;Loading and Running Programs&lt;/h3&gt;
&lt;p&gt;进程的创建和运行是通过 &lt;code&gt;exec&lt;/code&gt; 系列函数实现的：
$$
\mathtt{int}\quad \mathtt{execve(char;*filename,char;*argv[], char;*envp[])}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;filename&lt;/code&gt; 是可执行目标文件的路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;argv&lt;/code&gt;是参数列表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般满足：$\text{argv}[0]=\text{filename}$。&lt;/li&gt;
&lt;li&gt;必须以 NULL 结尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;envp&lt;/code&gt;是环境变量列表&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个条目式形如 $\mathtt{name=value}$ 的字符串。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对环境变量的操作有：&lt;code&gt;getenv()&lt;/code&gt;，&lt;code&gt;setenv()&lt;/code&gt;，&lt;code&gt;unsetenv()&lt;/code&gt; 等等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;必须以 NULL 结尾。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与 &lt;code&gt;exit()&lt;/code&gt; 一样，这个函数 &lt;strong&gt;调用一次，不返回&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;调用 execve 成功后，当前进程的 &lt;strong&gt;用户空间代码和数据&lt;/strong&gt; 会被指定的新程序所替换，但&lt;strong&gt;进程ID不会改变&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前进程的整个地址空间被清空，加载新程序的代码段、数据段、堆和栈。&lt;/li&gt;
&lt;li&gt;而如进程号（PID）、父进程号（PPID）、当前工作目录、用户ID、环境变量等大部分属性保持不变。&lt;/li&gt;
&lt;li&gt;execve 成功调用后不会返回；如果返回了，则说明出错了。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>用 SSH 连接希冀平台</title><link>https://www.tonyyin0418.com/blog/csapp/ssh</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/ssh</guid><description>利用公网可访问的云服务器和SSH反向隧道</description><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;内容由 AI 辅助完善，请注意甄别。&lt;/p&gt;
&lt;p&gt;希冀平台的桌面系统分辨率很低，还动不动就要重新连接，便捷性上不如直接用 SSH 连接到虚拟机上进行操作。&lt;/p&gt;
&lt;p&gt;由于希冀平台的虚拟机处于学院内网，外网无法直接访问，但虚拟机本身又可以访问公网，这种特殊的网络环境导致我们无法直接从外部通过 SSH 连接到虚拟机。&lt;/p&gt;
&lt;p&gt;为了解决这个限制，我们利用虚拟机能够访问公网的这一特点，通过在虚拟机上搭建 SSH 服务并借助反向 SSH 隧道，将虚拟机的 SSH 端口映射到云服务器（公网服务器）上。&lt;/p&gt;
&lt;p&gt;这样，我们就可以借助云服务器作为跳板，实现从外网安全便捷地访问处于内网的虚拟机。&lt;/p&gt;
&lt;p&gt;这里记录了通过反向 SSH 隧道实现云服务器跳板机访问的全过程。适合无 systemd 容器或极简系统，以及需要内网穿透的场景。以下是详细的配置过程和经验分享。&lt;/p&gt;
&lt;h2&gt;0. 我的环境&lt;/h2&gt;
&lt;p&gt;阿里云服务器，系统为 Ubuntu 24.04 LTS，有弹性公网 IP。&lt;/p&gt;
&lt;p&gt;请确保你的本机可以通过 SSH 正常连接这台跳板机，方法不在此展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 安装 SSH 服务&lt;/h2&gt;
&lt;p&gt;首先，更新软件源并安装 SSH 服务（openssh-server）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;apt update
apt install openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：在某些极简容器系统中，安装过程中可能会出现一些报错，且没有 systemd 管理服务，这些错误可以暂时忽略，后续手动启动 sshd 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. 手动启动 SSH 服务&lt;/h2&gt;
&lt;p&gt;由于没有 systemd，无法通过 &lt;code&gt;systemctl&lt;/code&gt; 启动服务，需要手动创建运行目录并启动 sshd：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p /var/run/sshd
/usr/sbin/sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以用下面命令验证 SSH 服务是否已启动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ps aux | grep sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. 配置 SSHD 服务参数&lt;/h2&gt;
&lt;p&gt;在希冀平台上，编辑 SSH 配置文件 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，确保内容包括：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Port 22
ListenAddress 0.0.0.0
ListenAddress ::
PermitRootLogin yes
PasswordAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在跳板云服务器上，编辑 SSH 配置文件 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，确保内容包括：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GatewayPorts yes
AllowTcpForwarding yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成修改后保存文件。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 重启 SSH 服务&lt;/h2&gt;
&lt;p&gt;因为没有 systemd，需要手工杀死并重新启动 sshd：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pkill sshd
/usr/sbin/sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. 设置 root 用户密码&lt;/h2&gt;
&lt;p&gt;给 root 用户设置一个密码，方便远程登录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;passwd root
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6. 配置反向 SSH 隧道&lt;/h2&gt;
&lt;p&gt;在本机执行反向隧道命令，将远程服务器端口（例：2222）映射为容器的 SSH 端口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -N -R \*:2222:localhost:22 root@yourip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用 &lt;code&gt;autossh&lt;/code&gt;，好像可以自动保持连接不断。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autossh -M 0 -N -o &quot;ServerAliveInterval=30&quot; -o &quot;ServerAliveCountMax=3&quot; -R \*:2222:localhost:22 root@yourip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果服务器不支持密码登录，建议先生成 SSH 密钥对方便免密登录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一路回车使用默认配置即可。然后查看公钥内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将公钥复制并粘贴到云服务器对应用户的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;p&gt;确认无误后，重新运行反向隧道命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -N -R \*:2222:localhost:22 root@yourip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不显示错误即表示连接成功。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 通过跳板机访问容器 SSH&lt;/h2&gt;
&lt;p&gt;登录云服务器后，通过本地端口转发或直接 ssh 连接到已映射的端口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -p 2222 root@localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;若之前修改了连接配置，避免因为 host key 变动导致连接失败，可先清理旧的 &lt;code&gt;known_hosts&lt;/code&gt; 记录：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -f ~/.ssh/known_hosts -R &apos;[localhost]:2222&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还可能因为，对于希冀平台上的不同实验，会打开不同机器。如果重复配置，也需要清理旧的记录。&lt;/p&gt;
&lt;p&gt;成功效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ssh-01.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 本地 VS Code 使用跳板机&lt;/h2&gt;
&lt;p&gt;还可以将云服务器设置为跳板机，配置 ProxyJump。这样可以直接在本机以一行指令连接，但是区别不大。&lt;/p&gt;
&lt;p&gt;具体方法可以询问大模型或参考网上教程。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 注意事项&lt;/h2&gt;
&lt;p&gt;平台貌似会定期休眠？原因不明。&lt;/p&gt;
&lt;p&gt;如果连接断了，需要先重新启动 sshd 服务，然后重新开启反向隧道。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/usr/sbin/sshd
ssh -N -R \*:2222:localhost:22 root@yourip
autossh -M 0 -N -o &quot;ServerAliveInterval=30&quot; -o &quot;ServerAliveCountMax=3&quot; -R \*:2222:localhost:22 root@yourip
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>链接</title><link>https://www.tonyyin0418.com/blog/csapp/linking</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/linking</guid><description>武汉大学2025春季学期 计算机系统基础 第七章 链接</description><pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why Linking&lt;/h2&gt;
&lt;p&gt;以一个简单的示例介绍链接过程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// main.c
int sum(int *a, int n);
int main() {
    // ...
}

// sum.c
int sum(int *a, int n) {
    // ...
    return s;
}

// 在命令行中
gcc -Og -o prog main.c sum.c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./linking-01.png&quot; alt=&quot;image-20250411100214447&quot;&gt;&lt;/p&gt;
&lt;p&gt;编译和链接的主要阶段包括以下部分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;cpp（预处理器）&lt;/strong&gt;：处理宏定义、头文件包含等预处理指令，生成&lt;strong&gt;中间文件&lt;/strong&gt; &lt;code&gt;main.i&lt;/code&gt; 和 &lt;code&gt;sum.i&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cc1（编译器）&lt;/strong&gt; ：将源代码翻译为&lt;strong&gt;汇编语言文件&lt;/strong&gt; &lt;code&gt;main.s&lt;/code&gt; 和 &lt;code&gt;sum.s&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;as（汇编器）&lt;/strong&gt; ：将汇编代码转换为&lt;strong&gt;可重定位目标文件&lt;/strong&gt; &lt;code&gt;main.o&lt;/code&gt; 和 &lt;code&gt;sum.o&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ld（连接器）&lt;/strong&gt; ：将多个可重定位目标文件链接成一个&lt;strong&gt;可执行文件&lt;/strong&gt; &lt;code&gt;sum&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分解的指令为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -E main.c -o main.i
gcc -S main.i -o main.s
gcc -c main.s -o main.o
# generate sum.o in the same way
gcc main.o sum.o -o prog
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Object Files&lt;/h2&gt;
&lt;p&gt;目标文件的主要形式包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.o&lt;/code&gt; 文件：可重定位目标文件。&lt;/li&gt;
&lt;li&gt;可执行文件：历史上曾经要求后缀名为 &lt;code&gt;.out&lt;/code&gt;，但可以无后缀，后缀名不重要。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.so&lt;/code&gt; 文件：共享对象文件，用于动态链接库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Executable and Linkable Format (ELF)&lt;/h2&gt;
&lt;p&gt;ELF 是一种标准的二进制文件格式，统一以下类型的文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可执行文件（Executable object files）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可重定位目标文件（Relocatable object files）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享库（Shared object files）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心转储（Core dumps）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ELF 结构&lt;/h3&gt;
&lt;p&gt;ELF 具有一种统一的格式，但根据用途不同显示不同的视图：&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;&lt;img src=&quot;./linking-02.png&quot; alt=&quot;image-20250411101647264&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rbakbashev/elfcat&quot;&gt;Github: elfcat&lt;/a&gt; 是一个有用的 ELF 文件可视化工具。&lt;/p&gt;
&lt;h3&gt;ELF 的关键组件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ELF Header&lt;/strong&gt;：描述文件的类型、目标架构、版本等基本信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Program Header Table&lt;/strong&gt;：描述可执行文件在内存中的各段布局。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Section Header Table&lt;/strong&gt;：描述各可重定位文件中的各节布局。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主要的节和段&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Section（节）&lt;/strong&gt;：最小的可处理单位，包含具体的代码和数据，在链接时加载：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.text&lt;/code&gt;：包含程序的机器指令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rodata&lt;/code&gt;：包含常量字符串等只读数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.data&lt;/code&gt;：包含初始化的全局变量和静态变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;/code&gt;：包含未初始化的全局变量和静态变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.symtab&lt;/code&gt;：包含程序中的符号信息，如函数名、变量名等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rel.text&lt;/code&gt; 和 &lt;code&gt;.rel.data&lt;/code&gt;：重定位信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.debug&lt;/code&gt;：包含调试信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Segment（段）&lt;/strong&gt;：可以由多个节构成，描述程序在内存中的布局，在运行时加载。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Readelf&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;readelf&lt;/code&gt; 是一个用于查看 ELF 文件内容的工具，常用的选项包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-h&lt;/code&gt;：查看 ELF 头信息。可以查看 &lt;code&gt;.o&lt;/code&gt; 文件和可执行文件的头信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-S&lt;/code&gt;：查看节头表信息。用于查看 &lt;code&gt;.o&lt;/code&gt; 的 Section 信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-l&lt;/code&gt;：查看程序头表信息。用于查看 Executable object file 的 Segment 信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-s&lt;/code&gt;：查看符号表信息。查看 symbol table.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step1: Symbol Resolution&lt;/h2&gt;
&lt;h3&gt;Linker Symbols&lt;/h3&gt;
&lt;p&gt;在链接过程中，符号是非常关键的部分。符号可以分为以下几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局符号（Global symbols）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Defined symbol&lt;/li&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;li&gt;
&lt;p&gt;&lt;strong&gt;外部符号（External symbols）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Undefined Symbaol&lt;/li&gt;
&lt;li&gt;定义在其他文件中，于当前文件中引用，需要在其他模块找到定义。&lt;/li&gt;
&lt;li&gt;定义在其他模块的外部全局变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;局部符号（Local symbols）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅在当前文件中定义和引用，无法被其他模块访问。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;static&lt;/code&gt; 关键字定义的 C 函数和全局变量。&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;ul&gt;
&lt;li&gt;Local non-static variables 存在寄存器或栈中，与linker无关。&lt;/li&gt;
&lt;li&gt;Local static variables 存在 &lt;code&gt;.data&lt;/code&gt; 或者 &lt;code&gt;.bss&lt;/code&gt; 段中，属于 linker 符号解析工作的一部分，在下面说明。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Symbol Resolution&lt;/h3&gt;
&lt;p&gt;在不同文件中定义了各种各样的符号，可能还有相同的符号名。&lt;/p&gt;
&lt;p&gt;Linker 的符号解析过程，就是要把这些符号解析成唯一的地址，供可执行文件使用。&lt;/p&gt;
&lt;h3&gt;gcc 阶段&lt;/h3&gt;
&lt;p&gt;gcc 对于一些特殊类型的变量，会存储到特殊段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;COMMON&lt;/code&gt;：未初始化的全局变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;/code&gt;：未初始化的静态变量，初始化为 0 的全局或静态变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;「Local Symbols」&lt;/strong&gt; 编译器确保局部符号的唯一性。因此无需Linker 特别协调，只需要存储局部符号的地址即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「Global Symbols」&lt;/strong&gt; 对于全局符号，每遇到一个弱全局符号，编译器不能确定用哪个定义，就先认为这个符号会出现在其他模块中，具体地：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码中类似 &lt;code&gt;int x;&lt;/code&gt;（未显式初始化）的全局变量，GCC在符号表中标记为&lt;code&gt;COMMON&lt;/code&gt;符号。&lt;/li&gt;
&lt;li&gt;这表示该符号“弱定义”，链接器允许多个目标文件中存在同名的&lt;code&gt;COMMON&lt;/code&gt;符号，先存到 COMMON 中，并最后将它们统一合并到最终的&lt;code&gt;.bss&lt;/code&gt;节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COMMON&lt;/code&gt;符号的大小和类型信息存储于符号表，等待链接器安排内存位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;强符号与弱符号&lt;/h3&gt;
&lt;p&gt;根据编译器规则可见，&lt;strong&gt;这部分只用于全局符号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了解决 Duplicate Symbol Definitions 同名符号，Linker 采用了以下策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Strong Symbols」&lt;/strong&gt; 已初始化的全局变量、全局函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Weak Symbols」&lt;/strong&gt; 未初始化的全局变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Linker 阶段&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;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// foo3.c
#include&amp;#x3C;stdio.h&gt;
void f(void);
int x = 12345;
int main()
{
    f();
    printf(&quot;x = %d\n&quot;, x);
    return 0;
}
// bar.c
int x;
void f() {
    x = 56789;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码中，&lt;code&gt;bar.c&lt;/code&gt; 中的 &lt;code&gt;int x&lt;/code&gt; 没有初始化，是弱符号，会被强符号取代。因此运行结果为 &lt;code&gt;x = 56789&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存在同名的弱符号时，随机选择一个。（目前编译器默认开启 &lt;code&gt;-fno-common&lt;/code&gt; 选项，同名若符号也会 Linker error）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// foo4.c
#include&amp;#x3C;stdio.h&gt;
void f(void);
int x;
int main()
{
    x = 12345;
    f();
    printf(&quot;x = %d\n&quot;, x);
    return 0;
}
// bar4.c
int x;
void f() {
    x = 56789;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要满足上述条件，Linker 就不会报错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./linking-03.png&quot; alt=&quot;image-20250420123543540&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Step2: Relocation&lt;/h2&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;ol&gt;
&lt;li&gt;节和符号定义：合并所有相同类型的 Section，给每个聚合节一个地址，&lt;strong&gt;让每条指令和全局变量都有唯一的运行时内存地址&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./linking-04.png&quot; alt=&quot;image-20250420131633287&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;符号引用： 修改代码节和数据节中，对符号的引用，指向正确的地址。比如 &lt;code&gt;call &amp;#x3C;address&gt;&lt;/code&gt; 的过程。这个部分基于 Relocation Entry.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ELF Relocation Entry&lt;/h3&gt;
&lt;p&gt;重定位条目是一个个结构体，存储在 &lt;code&gt;.rel&lt;/code&gt; 节中，包含以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「Offset 偏移量」&lt;/strong&gt; 指明需要修改的位置（比如指令中的立即数或地址）在它所属节中的偏移量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Symbol Index 符号索引」&lt;/strong&gt; 指明需要重定位的符号在符号表中的索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Type 类型」&lt;/strong&gt; 指明重定位的类型，分为绝对地址类型 R_X86_64_32、相对地址类型 R_X86_64_PC32 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「Addend 加数」&lt;/strong&gt; 指明需要加上的值，帮助链接器精确计算最终填充值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体可以在后面的例子中理解，先明确一下这两种重定位类型的区别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;「R_X86_64_PC32」&lt;/strong&gt; 相对寻址，需要填充的值是 &lt;strong&gt;目标符号的运行时地址加上 Addend&lt;/strong&gt;，再减去 &lt;strong&gt;重定位发生后程序计数器 PC 的值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「R_X86_64_32」&lt;/strong&gt; 绝对寻址，需要填充的值是 &lt;strong&gt;目标符号的运行时绝对地址&lt;/strong&gt; 加上 Addend。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用下面的例子来进一步解释：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* main.c */
int sum(int *a, int n);
int array[2] = {1, 2};
int main() 
{
    int val = sum(array, 2);
    return val;
}

// sum.c
int sum(int *a, int n)
{
    int i, s = 0;    
    for (i = 0; i &amp;#x3C; n; i++) { 
        s += a[i];
    }
    return s;
}        
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;0000000000000000 &amp;#x3C;main&gt;:
   0:	48 83 ec 08          	sub    $0x8,%rsp
   4:	be 02 00 00 00       	mov    $0x2,%esi
   9:	bf 00 00 00 00       	mov    $0x0,%edi
			a: R_X86_64_32	array
   e:	e8 00 00 00 00       	callq  13 &amp;#x3C;main+0x13&gt;
			f: R_X86_64_PC32	sum-0x4
  13:	48 83 c4 08          	add    $0x8,%rsp
  17:	c3                   	retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 Linker 在合并完所有目标文件后，确定了以下运行时地址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 函数（&lt;code&gt;.text&lt;/code&gt; 节起始）的地址为 &lt;code&gt;0x4004d0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sum&lt;/code&gt; 函数的地址为 &lt;code&gt;0x4004e8&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;array&lt;/code&gt; 变量（在 &lt;code&gt;.data&lt;/code&gt; 或 &lt;code&gt;.bss&lt;/code&gt; 节）的地址为 &lt;code&gt;0x601020&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PC 相对引用的计算方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 函数中调用 &lt;code&gt;sum&lt;/code&gt; 函数的指令 &lt;code&gt;callq 17 &amp;#x3C;main+0x17&gt;&lt;/code&gt; 实际上是 &lt;code&gt;e8&lt;/code&gt; 后面跟一个 32 位的相对偏移量。这条指令从 &lt;code&gt;main&lt;/code&gt; 节的偏移量 &lt;code&gt;0x12&lt;/code&gt; 开始，相对偏移量本身存储在偏移量 &lt;code&gt;0x13&lt;/code&gt; 到 &lt;code&gt;0x16&lt;/code&gt; 的 4 个字节中（当前值是 &lt;code&gt;00 00 00 00&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;相应的重定位条目会指向这个位置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;r.offset = 0xf
r.symbol = sum
r.type   = R_X86_64_PC32
r.addend = -4
# 下面会请关注 `offset` 和 `addend` 字段的用途。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;「Offset」&lt;/strong&gt; 字段的作用就是告诉 Linker 需要修改的值的位置，记为 $\mathtt{refaddr}$，计算方法为：
$$
\mathtt{refaddr}=\text{ADDR(r.main)}+\text{r.offset}
$$
代入具体数值计算：
$$
\mathtt{refaddr}=\mathtt{0x4004}+\mathtt{0xf}=\mathtt{0x4004df}
$$
&lt;strong&gt;「Addend」&lt;/strong&gt; 字段就是为了让 Linker 计算出需要填充到 $\mathtt{refaddr}$ 位置的值，记为 $\mathtt{*refptr}$，计算方法为：
$$
\mathtt{*refptr} = \text{ADDR(r.symbol)} + \text{r.addend} - \text{r.refaddr}
$$
代入具体数值计算：&lt;/p&gt;
&lt;p&gt;$$
\mathtt{*refptr}=\mathtt{0x4004e8}-4-\mathtt{0x4004df}=\mathtt{0x5}
$$&lt;/p&gt;
&lt;p&gt;Linker 会将 &lt;code&gt;0x5&lt;/code&gt; 这个值填充到 &lt;code&gt;0x4004d0 + 0xf&lt;/code&gt; 的位置。&lt;/p&gt;
&lt;p&gt;当 CPU 执行到 &lt;code&gt;callq&lt;/code&gt; 指令时，它会计算当前 PC 值 (&lt;code&gt;0x4004e3&lt;/code&gt;) 加上操作数 (&lt;code&gt;0x5&lt;/code&gt;)，得到 &lt;code&gt;0x4004e8&lt;/code&gt;，从而实现正确的函数调用跳转。&lt;/p&gt;
&lt;p&gt;PC 寄存器会存储下一条指令 &lt;code&gt;13: add $0x8,%rsp&lt;/code&gt; 的地址。$\text{PC}=\mathtt{0x4004d0+0x13=0x4004e3}$.&lt;/p&gt;
&lt;p&gt;Linker 并不知道 PC，所以才需要编译器提供这个 r.addend 辅助。&lt;/p&gt;
&lt;h3&gt;绝对引用&lt;/h3&gt;
&lt;p&gt;这部分相对就很简单了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;r.offset = 0xa
r.symbol = array
r.type   = R_X86_64_32
r.addend = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;offset&lt;/code&gt; 的含义不变，&lt;code&gt;addend&lt;/code&gt; 完全用不上。&lt;/p&gt;
&lt;p&gt;填入的地址直接就是 $\text{ADDR(array)}$，由于 &lt;code&gt;array&lt;/code&gt; 是全局变量，Linker 会在 &lt;code&gt;.data&lt;/code&gt; 段中找到它的地址。&lt;/p&gt;
&lt;h2&gt;Loading&lt;/h2&gt;
&lt;p&gt;Loading Executable Object Files 加载可执行目标文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./linking-05.png&quot; alt=&quot;image-20250420182810254&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Libraries&lt;/h2&gt;
&lt;p&gt;分为静态和动态两种。&lt;/p&gt;
&lt;p&gt;静态库就用于前面讲到的静态链接的两个步骤中，而动态库用于动态链接。&lt;/p&gt;
&lt;h3&gt;Static Libraries&lt;/h3&gt;
&lt;p&gt;把所有相关的目标文件打包成单独文件，来避免编译器重复编译。&lt;/p&gt;
&lt;p&gt;静态库的后缀为 &lt;code&gt;.a&lt;/code&gt;，在 Linux 中使用 &lt;code&gt;ar&lt;/code&gt; 命令创建。&lt;/p&gt;
&lt;p&gt;在使用 &lt;code&gt;gcc&lt;/code&gt; 创建可执行文件时，可以直接指定静态库的路径和名称，编译器会自动链接。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -c main.c
gcc -o program main.c libx.a liby.a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个很重要的话题也很令人迷惑的话题是，链接器会在符号解析阶段，按照命令行输入的顺序，从左到右依次链接静态库。因此，不同的顺序会导致不同的结果，错误的顺序可能会导致符号解析错误。&lt;/p&gt;
&lt;p&gt;下面描述一下具体过程。&lt;/p&gt;
&lt;p&gt;定义集合 $U$ 存储未被解析的符号，集合 $E$ 存储可重定位目标文件，集合 $D$ 存储已被解析的符号。&lt;/p&gt;
&lt;p&gt;之后从左到右依次处理，若当前文件为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标文件 $f$&lt;/strong&gt;，直接加入 $E$，然后对于其中的每个符号 $s$，不断调整集合 $U$ 和 $D$。用 $s$ 来匹配 $U$ 中出现的未定义符号，也可能 $s$ 是未定义符号，需要加入 $U$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态库存档文件 $f$&lt;/strong&gt;，如果某个成员 $m$ 定义了 $U$ 中的引用，则把 $m$ 加到 $E$ 中，而不是整个静态库，之后更新 $U$ 和 $D$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于比较复杂的情况，比如  &lt;code&gt;foo.c&lt;/code&gt; 调用 &lt;code&gt;libx.a&lt;/code&gt;，&lt;code&gt;libx.a&lt;/code&gt; 调用 &lt;code&gt;liby.a&lt;/code&gt;，&lt;code&gt;liby.a&lt;/code&gt;又调用 &lt;code&gt;libx.a&lt;/code&gt;，则：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc foo.c libx.a liby.a libx.a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以重复出现 &lt;code&gt;libx.a&lt;/code&gt; 来满足依赖。&lt;/p&gt;
&lt;h3&gt;Shared Libraries&lt;/h3&gt;
&lt;p&gt;共享库，主要用于在动态链接过程中。&lt;/p&gt;
&lt;p&gt;共享库是解决静态库缺陷的现代创新产物，可以作为模块被加载到任意内存地址。&lt;/p&gt;
&lt;p&gt;共享库是一组可被多个程序共享使用的二进制代码文件，通常以&lt;code&gt;.so&lt;/code&gt;作为文件后缀（Linux下）。这些库文件包含位置无关的代码（PIC），以便于被加载到进程内存的任意位置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「编译共享库」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;生成共享库时，必须使用&lt;code&gt;-fPIC&lt;/code&gt;（Position Independent Code）选项，确保代码不依赖固定地址。&lt;/p&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -fPIC -c foo.c -o foo.o
gcc -shared -o libfoo.so foo.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的&lt;code&gt;libfoo.so&lt;/code&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;加载时，动态链接器才会将共享库加载到内存中，并解析符号引用，之后再控制转移给程序。此刻，共享库的位置才是确定的。&lt;/p&gt;
&lt;h2&gt;Library interpositioning&lt;/h2&gt;
&lt;p&gt;库打桩技术。&lt;/p&gt;
&lt;p&gt;截获所有某个函数的调用，进而实现一些操作。&lt;/p&gt;
&lt;h3&gt;Applications&lt;/h3&gt;
&lt;p&gt;下面是库打桩在 &lt;strong&gt;「Monitoring and Profiling」&lt;/strong&gt; 方面的应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记录函数调用次数&lt;/li&gt;
&lt;li&gt;记录函数调用位置和参数&lt;/li&gt;
&lt;li&gt;内存监控（内存泄漏、内存访问的 trace）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;p&gt;库打桩可以在三个阶段进行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compile-time&lt;/strong&gt; ：在编译时插入打桩代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Link-time&lt;/strong&gt; ：在链接时插入打桩代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run-time&lt;/strong&gt; ：在运行时插入打桩代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共同的实现逻辑是，类似于自己创建一个函数覆盖原本的库函数，来增加所需功能。只是根据条件不同，需要在不同阶段插入，也就有不同的代码实现方式。&lt;/p&gt;
&lt;p&gt;其中，&lt;strong&gt;Run-time&lt;/strong&gt; 方法不需要知道源代码，可以自己额外重写函数，直接在运行时插入打桩代码。&lt;/p&gt;
&lt;h2&gt;PIC 位置无关代码&lt;/h2&gt;
&lt;p&gt;Position-Independent Code, PIC.&lt;/p&gt;
&lt;p&gt;在上面讲过的 Relocation 过程中，PC32 的&lt;strong&gt;相对寻址&lt;/strong&gt;条目对应的代码段一定是 &lt;strong&gt;PIC&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，对于 PIC 模块，可以把代码段加载到内存的任何位置，而无需链接器做任何修改。这使得共享库可以在运行时被加载到内存的任何位置，而不需要重新编译。&lt;/p&gt;
&lt;p&gt;因此，动态链接和共享库代码一定要实现 PIC，还可用于随机化地址空间。&lt;/p&gt;
&lt;h3&gt;生成 PIC&lt;/h3&gt;
&lt;p&gt;根据 gcc 选项 &lt;code&gt;-fpic&lt;/code&gt; 或 &lt;code&gt;-fPIC&lt;/code&gt;，略有区别。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-fPIC&lt;/code&gt;更“完全”，通常用于64位平台共享库编译。&lt;/p&gt;
&lt;h3&gt;PIC 数据引用&lt;/h3&gt;
&lt;p&gt;在共享库中定义的变量，由于不知道共享库会被加载到哪里。根据动态链接的过程，不会把这些符号拷贝到可执行文件中，因此代码段并不能事先知道变量和函数的地址。&lt;/p&gt;
&lt;p&gt;但是，我们又不能在执行时动态修改代码段，要保证代码段可以预先编译好不用变。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，编译器会在数据引用时使用 &lt;strong&gt;「全局偏移量表 Global Offset Table, GOT」&lt;/strong&gt;，在 &lt;code&gt;.data&lt;/code&gt; 段中。&lt;/p&gt;
&lt;p&gt;在 GOT 表中，每个全局符号都有一项，值为全局符号的地址。这个值在运行之前也是未知的，会在运行过程中被动态链接器填充。如此一来，只需在得知符号地址后，修改 GOT 中相关项的值即可。&lt;/p&gt;
&lt;p&gt;具体地，在预先编译出的代码段中，符号地址的填充值为 &lt;strong&gt;「GOT 表中某一项的地址」&lt;/strong&gt;。在执行代码时，代码会从 GOT 表中读取符号的地址，然后再进行实际的调用。&lt;/p&gt;
&lt;p&gt;另外，GOT 中的地址是相对地址，因此可以在运行时被动态链接器修改。&lt;/p&gt;
&lt;h3&gt;PIC 函数调用&lt;/h3&gt;
&lt;p&gt;PIC 的方法为 &lt;strong&gt;「Lazy binding 延迟绑定」&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;需要 &lt;strong&gt;「过程链接表 Procedure Linkage Table」&lt;/strong&gt; 辅助，在 &lt;code&gt;.text&lt;/code&gt; 段中。&lt;/p&gt;
&lt;p&gt;GOT 表 与 PLT 表协同，完成 PIC 函数调用指令，下面是具体流程与示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./linking-06.png&quot; alt=&quot;image-20250422144033131&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次调用函数时，程序进入 $\mathtt{PLT[2]}$，然后经过 $\mathtt{GOT[4]}$ 跳转到第二条指令，没有什么意义。&lt;/li&gt;
&lt;li&gt;随后，把动态链接器所需的各种条目压入栈中，它会重写 GOT 表中的值，改为函数的实际地址。&lt;/li&gt;
&lt;li&gt;之后再次调用函数，先控制转移到 $\mathtt{PLT[2]}$，之后通过 $\mathtt{GOT[4]}$ 直接跳转到实际地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面例子中，有关 $\mathtt{PLT[0-1], GOT[0-3]}$ 的详细作用可以看书。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;</content:encoded></item><item><title>多维随机变量及其分布</title><link>https://www.tonyyin0418.com/blog/probability/chap-3</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-3</guid><description>武汉大学2025春季学期 概率论与数理统计 第三章 多维随机变量及其分布</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第三章：多维随机变量及其分布。&lt;/p&gt;
&lt;p&gt;本章主要讨论两个或两个以上的随机变量。&lt;/p&gt;
&lt;h2&gt;二维随机变量&lt;/h2&gt;
&lt;p&gt;二维随机变量 $(X, Y)$ 看作整体进行研究。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「联合分布函数」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;定义二元函数：
$$
F(x, y)=P{(X\leq x)\cap (Y\leq y)} \xlongequal[]{\text{written as}}P{X\leq x, Y\leq y}
$$
为二维随机变量 $(X,Y)$ 的&lt;strong&gt;分布函数&lt;/strong&gt;，或随机变量 $X$ 和 $Y$ 的&lt;strong&gt;联合分布函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一些性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于 $x$ 和 $y$ 单调不减。&lt;/li&gt;
&lt;li&gt;$0\leq F(x, y)\leq 1$.&lt;/li&gt;
&lt;li&gt;$F(x, y)$ 关于 $x$ 右连续，关于 $y$ 右连续。&lt;/li&gt;
&lt;li&gt;对于任意 $(x_1, y_1)$, $(x2,y2)$, $x_1&amp;#x3C;x_2$, $y_1&amp;#x3C;y_2$，有：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
F(x_2, y_2)-F(x_1, y_2)-F(x_2, y_1)+F(x_1, y_1)=P{x_1&amp;#x3C;X\leq x_2, y_1&amp;#x3C;Y\leq y_2}\geq 0
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「联合概率密度」&lt;/strong&gt;
$$
F(x, y)=\int_{-\infty}^{y}\int_{-\infty}^{x}f(u, v)\text{d}u\text{d}v
$$&lt;/p&gt;
&lt;p&gt;其中 $f(x, y)$ 称为 $(X, Y)$ 的&lt;strong&gt;概率密度&lt;/strong&gt;，或 $X$ 和 $Y$ 的&lt;strong&gt;联合概率密度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一些性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$f(x,y)\geq 0$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}f(x,y)\text{d}x\text{d}y=F(\infty,\infty)=1$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$P{(X, Y)\in G}=\iint\limits_{G}f(x, y)\text{d}x\text{d}y$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若在 $(x, y)$ 处连续，则：
$$
\frac{\partial^2F(x, y)}{\partial x\partial y}=f(x, y)
$$
用处是，可推知：
$$
P{x&amp;#x3C;X\leq x+\Delta x, y&amp;#x3C;Y\leq y+\Delta y}\simeq f(x, y)\Delta x\Delta y
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;边缘分布&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「边缘分布函数」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $(X, Y)$ 的分布函数为 $F(x, y)$，则 $X$ 的&lt;strong&gt;边缘分布函数&lt;/strong&gt;为：&lt;/p&gt;
&lt;p&gt;$$
F_X(x)=P{X\leq x}=\lim_{y\to \infty}F(x, y)=F(x, \infty)
$$&lt;/p&gt;
&lt;p&gt;即：&lt;/p&gt;
&lt;p&gt;$$
F_X(x)=F(x, \infty),\quad F_{Y}(y)=F(\infty,y)
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「边缘概率密度」&lt;/strong&gt;
$$
f_X(x)=\int_{-\infty}^{\infty}f(x, y)\text{d}y
$$&lt;/p&gt;
&lt;p&gt;$$
f_Y(y)=\int_{-\infty}^{\infty}f(x, y)\text{d}x
$$&lt;/p&gt;
&lt;h3&gt;二维正态分布&lt;/h3&gt;
&lt;p&gt;不是考察重点。&lt;/p&gt;
&lt;h2&gt;条件分布&lt;/h2&gt;
&lt;p&gt;讨论二维随机变量的条件概率分布。&lt;/p&gt;
&lt;h3&gt;离散型随机变量&lt;/h3&gt;
&lt;p&gt;离散型很好理解，跟普通的条件概率很像。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「条件分布律」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于二维离散型随机变量 $(X, Y)$，称：
$$
P{X=x_i \mid Y=y_j }=\frac{P{X=x_i,Y=y_j}}{p{Y=y_j}}=\frac{p_{ij}}{p_{\cdot j}},\quad i=1,2\cdots
$$
为在 $Y=y_j$ 条件下随机变量 $X$ 的条件分布律。&lt;/p&gt;
&lt;h3&gt;连续型随机变量&lt;/h3&gt;
&lt;p&gt;对任意 $x,y$ 都有 $P{X=x}=0,P{Y=y}=0$，所以不能直接用条件概率公式，而是&lt;strong&gt;用极限定义&lt;/strong&gt;如下：&lt;/p&gt;
&lt;p&gt;设 $(X, Y)$ 的概率密度为 $f(x, y)$，$(X, Y)$ 关于 $Y$ 的边缘密度函数为 $f_Y(y)$，则有条件概率：
$$
P{X\leq x\mid y&amp;#x3C;Y\leq y+\epsilon} =\frac{P{X\leq x, y&amp;#x3C;Y\leq y+\epsilon}}{P{y&amp;#x3C;Y\leq y+\epsilon}}=\frac{\int_{-\infty}^{x}[\int_{y}^{y+\epsilon}f(x,y)\text{d}y]\text{d}x}{\int_{y}^{y+\epsilon}f_{Y}(y)\text{d}y}
$$
当 $\epsilon \rightarrow 0$ 时，可推得连续性随机变量的相关定义式，对于二维连续型随机变量 $(X, Y)$，称：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「条件概率分布函数」&lt;/strong&gt;
$$
F_{x\mid y}(x\mid y)=P{X\leq x\mid Y=y}=\int_{-\infty}^{y}\frac{f(x, y)}{f_{Y}(y)}\text{d}x
$$
&lt;strong&gt;「条件概率密度」&lt;/strong&gt;
$$
f_{x\mid y}(x\mid y)=\frac{f(x ,y)}{f_{Y}(y)}
$$&lt;/p&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;P88：设随机变量 $(X, Y)$ 的概率密度为：
$$
f(x, y)=\left{\begin{array}{rl}
1, &amp;#x26; |y|&amp;#x3C;x, 0&amp;#x3C;x&amp;#x3C;1 \
0, &amp;#x26; \text{Otherwise}
\end{array}\right.
$$
求条件概率密度 $f_{Y\mid X}(y\mid x)$，$f_{X\mid Y}(x\mid y)$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;先求 $f_X$ 和 $f_Y$，
$$
f_X(x)=\int_{-\infty}^{\infty}f(x, y)\text{d}y=\int_{-x}^{x}1\text{d}y=2x\quad (0&amp;#x3C;x&amp;#x3C;1)
$$&lt;/p&gt;
&lt;p&gt;$$
f_Y(y)=\int_{-\infty}^{\infty}f(x, y)\text{d}x=\int_{|y|}^{1}1\text{d}x=1-|y|\quad (|y|&amp;#x3C;1)
$$&lt;/p&gt;
&lt;p&gt;之后求条件概率密度，
$$
f_{Y\mid X}(y\mid x)=\frac{f(x, y)}{f_{X}(x)}=\frac{1}{2x},\quad (|y|&amp;#x3C;x, 0&amp;#x3C;x&amp;#x3C;1)
$$&lt;/p&gt;
&lt;p&gt;$$
f_{X\mid Y}(x\mid y)=\frac{f(x, y)}{f_{Y}(y)}=\frac{1}{1-|y|},\quad (|y|1)
$$&lt;/p&gt;
&lt;h2&gt;独立性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「定义」&lt;/strong&gt;
$$
P{X\leq x,Y\leq y}=P{X\leq x}P{Y\leq y}
$$
等价于：
$$
F(x, y)=F_X(x)F_Y(y)
$$
&lt;strong&gt;「连续型随机变量」&lt;/strong&gt; 下式处处成立
$$
f(x, y)=f_X(x)f_Y(y)
$$
&lt;strong&gt;「离散型随机变量」&lt;/strong&gt; 下式处处成立
$$
P{X=x_i,Y=y_j}=P{X=x_i}P{Y=y_j}
$$&lt;/p&gt;
&lt;h2&gt;多个随机变量的分布&lt;/h2&gt;
&lt;h3&gt;和的分布&lt;/h3&gt;
&lt;p&gt;即 $Z=X+Y$ 的分布。&lt;/p&gt;
&lt;p&gt;设 $(X, Y)$ 是二维连续性随机变量，则 $Z=X+Y$ 仍为连续性随机变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分布函数」&lt;/strong&gt;
$$
F_{Z}(z)=P{Z\leq z}=\iint\limits_{x+y\leq z}f(x, y)\text{d}x\text{d}y
$$
画图，转化为累次积分：
$$
\begin{aligned}
F_{Z}(z)
&amp;#x26;=\int_{-\infty}^{\infty}\text{d}y\int_{-\infty}^{z-y}f(x,y)\text{d}x\
&amp;#x26;=\int_{-\infty}^{\infty}\text{d}y\int_{-\infty}^{z}f(u-y,y)\text{d}u\
&amp;#x26;=\int_{-\infty}^{z}[\int_{-\infty}^{\infty}f(u-y,y)\text{d}y]\text{d}u\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「概率密度」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
f_{X+Y}(z)=\int_{-\infty}^{\infty}f(z-y,y)\text{d}y
$$
或同理可得：
$$
f_{X+Y}(z)=\int_{-\infty}^{\infty}f(x,z-x)\text{d}x
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「卷积公式」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;若 $X$ 和 $Y$ 独立，则 $f(x, y)=f_{X}(x)\cdot f_{Y}(y)$，代入得到：
$$
f_{X+Y}(z)=\int_{-\infty}^{\infty}f_X(z-y)f_{Y}(y)\text{d}y
$$
和
$$
f_{X+Y}(z)=\int_{-\infty}^{\infty}f_X(x)f_{Y}(z-x)\text{d}x
$$
称为 $f_{X}$ 和 $f_{Y}$ 的&lt;strong&gt;卷积公式&lt;/strong&gt;，记为 $f_{X}\ast f_{Y}$，是重要公式。&lt;/p&gt;
&lt;h3&gt;商的分布、积的分布&lt;/h3&gt;
&lt;p&gt;$$
f_{Y/X}(z)=\int_{-\infty}^{\infty}\mid x \mid f(x,xz)\text{d}x
$$&lt;/p&gt;
&lt;p&gt;$$
f_{XY}(z)=\int_{-\infty}^{\infty}\frac{1}{\mid x \mid} f(x,\frac{z}{x})\text{d}x
$$&lt;/p&gt;
&lt;p&gt;若 $X$ 和 $Y$ 独立，也可以用上述类似的方式进行拆为两个函数的乘积，把 $f(x, y)=f_X(x)\cdot f_Y(y)$ 代入，
$$
f_{Y/X}(z)=\int_{-\infty}^{\infty}\mid x \mid f_X(x)\cdot f_Y(xz)\text{d}x
$$&lt;/p&gt;
&lt;p&gt;$$
f_{XY}(z)=\int_{-\infty}^{\infty}\frac{1}{\mid x \mid} f_X(x)\cdot f_Y(\frac{z}{x})\text{d}x
$$&lt;/p&gt;
&lt;h3&gt;Min / Max 分布&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;「二元情形」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先考虑二元情形 $M=\max{X, Y}$ 及 $N=\min{X, Y}$ 的分布函数，&lt;strong&gt;其中 $X, Y$ 独立&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于 $M$ 是最大值，$M\leq z$ 等价于 $X$ 和 $Y$ 都 $\leq z$，固有：
$$
F_{\max}(z)=P{M\leq z}=P{X\leq z}P{Y\leq z}=F_{X}(z)\cdot F_{Y}(z)
$$
类似可得 $N=\min{X, Y}$ 最小值的分布函数：
$$
F_{\min}(z)=1-P{X&gt;z,Y&gt;z}=1-[1-F_{X}(z)][1-F_{Y}(z)]
$$
&lt;strong&gt;「多元情形」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面推广到 $n$ 个独立随机变量的情况。&lt;/p&gt;
&lt;p&gt;对于 $n$ 个相互独立的随机变量 $X1, X2, \cdots, X_n$，有：
$$
F_{\max}(z)=F_{X_1}(z)\cdot F_{X_2}(z)\cdots F_{X_n}(z).
$$&lt;/p&gt;
&lt;p&gt;$$
F_{\min}(z)=1-[1-F_{X_1}(z)]\cdot [1-F_{X_2}(z)]\cdots [1-F_{X_n}(z)].
$$&lt;/p&gt;
&lt;h3&gt;Rayleigh 瑞利分布&lt;/h3&gt;
&lt;p&gt;若 $(X,Y)\sim N(0,0,1,1,0)$，求 $Z=\sqrt{X^2+Y^2}$ 的分布。
$$
f(x,y)=\frac{1}{2\pi}e^{-\frac{x^2+y^2}{2}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分布函数」&lt;/strong&gt;
$$
F_Z(z)=P{x^2+y^2\leq z^2}=\iint\limits_{D}f(x, y)\text{d}x\text{dy}
$$&lt;/p&gt;
&lt;p&gt;计算积分：
$$
\begin{aligned}
F_Z(z)&amp;#x26;=\int_{0}^{2\pi}\text{d}\theta\int_{0}^{z}f(r\cos\theta,r\sin\theta)r\text{d}r\
&amp;#x26;=\int_{0}^{2\pi}\text{d}\theta\int_{0}^{z}\frac{1}{2\pi}e^{-\frac{r^2}{2}}r\text{d}r \
&amp;#x26;=\int_{0}^{2\pi}\frac{1}{2\pi}e^{-\frac{z^2}{2}}(1-e^{-\frac{z^2}{2}})\text{d}\theta\
&amp;#x26;=\frac{1}{2\pi}e^{-\frac{z^2}{2}}(1-e^{-\frac{z^2}{2}})2\pi\
&amp;#x26;=1-e^{-\frac{z^2}{2}}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;即：
$$
F_Z(z)=\left{
\begin{array}{ll}
1-e^{-\frac{z^2}{2}}, &amp;#x26;z&gt;0\
0 ,&amp;#x26;z&amp;#x3C;0
\end{array}
\right.
$$
&lt;strong&gt;「概率密度」&lt;/strong&gt; 对分布函数求导，得：
$$
f_Z(z)=\left{
\begin{array}{ll}
ze^{-\frac{z^2}{2}}, &amp;#x26;z&gt;0\
0 ,&amp;#x26;z&amp;#x3C;0
\end{array}
\right.
$$&lt;/p&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;P89：设 $X$ 和 $Y$ 是两个&lt;strong&gt;相互独立&lt;/strong&gt;的随机变量，其概率密度分别为：
$$
f_X(x)=1,\quad0\leq x\leq1
$$
$$
f_Y(y)=e^{-y},\quad y&gt;0
$$&lt;/p&gt;
&lt;p&gt;求随机变量 $Z=X+Y$ 的概率密度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先要画图，并且考虑 $Z$ 的取值，需满足：
$$
\left{\begin{array}{l}
y\leq z\leq y+1,\
y&gt;0
\end{array}\right.
$$
图形为：&lt;/p&gt;
&lt;p&gt;利用公式积分：
$$
f_Z(z)=\int_{-\infty}^{\infty}f_X(z-y)f_Y(y)\text{d}y
$$&lt;/p&gt;
&lt;p&gt;$$
f_Z(z)=\left{\begin{array}{ll}
\displaystyle \int_{0}^{z}1\cdot e^{-y}\text{d}y=1-e^{-z}, &amp;#x26;0&amp;#x3C;z&amp;#x3C;1\
\displaystyle \int_{z-1}^{z}1\cdot e^{-y}\text{d}y=(e-1)e^{-z}, &amp;#x26;z\geq 1\
0,&amp;#x26;\text{Otherwise}
\end{array}\right.
$$&lt;/p&gt;</content:encoded></item><item><title>计算机系统基础 期中 Cheat-sheet</title><link>https://www.tonyyin0418.com/blog/csapp/cheat-sheet</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/cheat-sheet</guid><description>武汉大学2025春季学期 计算机系统基础 期中考试复习</description><pubDate>Wed, 02 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;跟皮哥一起写了一下，仓库链接在 &lt;a href=&quot;https://github.com/TonyYin0418/csapp-cheat-sheet&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cheat-sheet-1.svg&quot; alt=&quot;Page 1 of PDF&quot;&gt;
&lt;img src=&quot;./cheat-sheet-2.svg&quot; alt=&quot;Page 2 of PDF&quot;&gt;&lt;/p&gt;</content:encoded></item><item><title>随机变量及其分布</title><link>https://www.tonyyin0418.com/blog/probability/chap-2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/probability/chap-2</guid><description>武汉大学2025春季学期 概率论与数理统计 第二章 随机变量及其分布</description><pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&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; 设随机试验的样本空间为 $S={e}$，$X=X(e)$ 是定义在样本空间 $S$ 上的实体单值函数，称 $X=X(e)$ 为随机变量。&lt;/p&gt;
&lt;p&gt;一般用大写字母 $X, Y, Z, W\dots$ 代表随机变量，用小写字母 $x, y, z, w$ 代表实数。&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;strong&gt;离散型随机变量&lt;/strong&gt;的讨论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「定义」&lt;/strong&gt; 设随机变量 $X$ 的取值范围为 $x_1, x_2, \dots, x_n$，$p_i=P(X=x_i)$，$i=1, 2, \dots, n$，则称 $p_i$ 为随机变量 $X$ 的分布律。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「性质」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$p_i \geq 0$，$i=1, 2, \dots, n$，&lt;/li&gt;
&lt;li&gt;$\sum\limits_{i=1}^n p_i = 1$.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0-1 分布&lt;/h3&gt;
&lt;p&gt;又称两点分布。&lt;/p&gt;
&lt;p&gt;设随机变量 $X$ 只可能取 $0$ 和 $1$ 两个值，分布律为：
$$
\begin{array}{c|cc}
X &amp;#x26; 0 &amp;#x26; 1\ \hline
p_i &amp;#x26; 1-p &amp;#x26; p
\end{array}
$$&lt;/p&gt;
&lt;h3&gt;二项分布&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;「伯努利试验」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设试验 $E$ 只有两个可能结果：$A$ 及 $\bar A$。&lt;/p&gt;
&lt;p&gt;分布律与两点分布相同，即：$p(A)=p,p(\bar A)=1-p$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「$n$ 重伯努利试验」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将 $E$ 独立重复地进行 $n$ 次。分布律即&lt;strong&gt;二项分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「二项分布」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设随机变量 $X$ 表示 $n$ 次独立重复的伯努利试验中事件 $A$ 发生的次数。&lt;/p&gt;
&lt;p&gt;分布律为：
$$
p_i = P{X=i} = \binom{n}{i}\cdot p^i(1-p)^{n-i}, \quad i=0, 1, \dots, n
$$&lt;/p&gt;
&lt;p&gt;则称 $X$ 服从参数为 $n,p$ 的二项分布，记作 $X \sim B(n,p)$。&lt;/p&gt;
&lt;h3&gt;泊松分布&lt;/h3&gt;
&lt;p&gt;设随机变量 $X$ 取值概率为：&lt;/p&gt;
&lt;p&gt;$$
p_i = P{X=i} = \frac{\lambda^i}{i!}e^{-\lambda}, \quad i=0, 1, 2, \dots
$$
其中 $\lambda&gt;0$ 为常数，则称 $X$ 服从参数为 $\lambda$ 的泊松分布，记作 $X \sim \pi(\lambda)$ 或 $X\sim P(\lambda)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「泊松定理」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $\lambda&gt;0$ 是常数，$n$ 是任意正整数，设 $np_n=\lambda$，则对于任意固定的非负整数 $k$，有：&lt;/p&gt;
&lt;p&gt;$$
\lim_{n \to \infty}\binom{n}{k}p_{n}^k(1-p_n)^{n-k} = \frac{\lambda^k}{k!}e^{-\lambda}
$$
这是一个用泊松分布来逼近二项分布的定理。&lt;/p&gt;
&lt;h2&gt;分布函数&lt;/h2&gt;
&lt;p&gt;设 $X$ 是一个随机变量，$x$ 是任意实数，函数：
$$
F(x)=P{X\leq x},\quad -\infty&amp;#x3C;x&amp;#x3C;\infty
$$
为 $X$ 的&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; 若对于分布函数 $F_{X}(x)$，存在非负可积函数 $f(x)$ 使：
$$
F_X(x)=\int_{-\infty}^{x}f(t)\text{d}t
$$
则称 $f(x)$ 为随机变量 $X$ 的&lt;strong&gt;概率密度函数&lt;/strong&gt;，简称&lt;strong&gt;概率密度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这种变量 $X$ 就是&lt;strong&gt;连续型随机变量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面是三种重要的连续型随机变量。&lt;/p&gt;
&lt;h3&gt;均匀分布&lt;/h3&gt;
&lt;p&gt;记为 $X\sim U(a, b)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「概率密度」&lt;/strong&gt;
$$
f(x)=\left{
\begin{aligned}
\displaystyle\frac{1}{b-a}, &amp;#x26; a&amp;#x3C;x&amp;#x3C;b\
0, &amp;#x26; \text{Otherwise}
\end{aligned}\right.
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分布函数」&lt;/strong&gt;
$$
F(x)=\left{
\begin{aligned}
0, &amp;#x26;x&amp;#x3C;a\
\displaystyle\frac{x-a}{b-a}, &amp;#x26; a\leq x&amp;#x3C;b\
1, &amp;#x26; x\geq b
\end{aligned}\right.
$$&lt;/p&gt;
&lt;h3&gt;指数分布&lt;/h3&gt;
&lt;p&gt;记为 $X\sim E(\lambda)$ 或 $X\sim \text{Exp}(\lambda)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「概率密度」&lt;/strong&gt;
$$
f(x)=\left{
\begin{aligned}
\frac{1}{\lambda}e^{-\frac{x}{\lambda}}, &amp;#x26; x&gt;0\
0, &amp;#x26; \text{Otherwise}
\end{aligned}\right.
$$
&lt;strong&gt;「分布函数」&lt;/strong&gt;
$$
F(x)=\left{
\begin{aligned}
1-e^{-\frac{x}{\lambda}}, &amp;#x26; x&gt;0\
0, &amp;#x26; \text{Otherwise}
\end{aligned}\right.
$$&lt;/p&gt;
&lt;h3&gt;正态分布&lt;/h3&gt;
&lt;p&gt;记为 $X\sim N(\mu, \sigma^2)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「概率密度」&lt;/strong&gt;
$$
f(x)=\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{(x-\mu)^2}{2\sigma^2}}, \quad -\infty&amp;#x3C;x&amp;#x3C;\infty
$$
其中 $\mu$ 是常数，$\sigma&gt;0$ 是常数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「分布函数」&lt;/strong&gt;
$$
F(x)=\frac{1}{\sqrt{2\pi}\sigma}\int_{-\infty}^{x}e^{-\frac{(t-\mu)^2}{2\sigma^2}}\text{d}t
$$&lt;/p&gt;
&lt;p&gt;不存在解析表达式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「标准正态分布」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当 $\mu=0,\sigma=1$ 时，称随机变量 $X$ 服从&lt;strong&gt;标准正态分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此时概率密度用 $\varphi(x)$ 表示，分布函数用 $\varPhi(x)$ 表示，代入有：
$$
\varphi(x)=\frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}
$$&lt;/p&gt;
&lt;p&gt;$$
\varPhi(x)=\frac{1}{\sqrt{2\pi}}\int_{-\infty}^{x}e^{-\frac{t^2}{2}}\text{d}t
$$&lt;/p&gt;
&lt;p&gt;并且有性质：
$$
\varPhi(-x)=1-\varPhi(x)
$$
&lt;/p&gt;
&lt;p&gt;对于 $\varPhi(x)$，有很多函数表可供查询使用。&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;strong&gt;引理&lt;/strong&gt;：若随机变量 $X\sim N(\mu, \sigma^2)$，则 $Z=\frac{X-\mu}{\sigma}\sim N(0, 1)$，即可标准化。&lt;/p&gt;
&lt;p&gt;于是若 $X\sim N(\mu, \sigma^2)$，其分布函数 $F(x)$ 可以写为：
$$
F(x)=P{\frac{X-\mu}{\sigma}\leq \frac{x-\mu}{\sigma}}=\varPhi(\frac{x-\mu}{\sigma})
$$
进一步查表即可。&lt;/p&gt;</content:encoded></item><item><title>程序的机器级表示 2</title><link>https://www.tonyyin0418.com/blog/csapp/machine2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/machine2</guid><description>武汉大学2025春季学期 计算机系统基础 第三章 程序的机器级表示 2 控制</description><pubDate>Tue, 18 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;程序的机器级表示 2&lt;/h2&gt;
&lt;p&gt;Machine control&lt;/p&gt;
&lt;h2&gt;控制：条件码寄存器&lt;/h2&gt;
&lt;p&gt;Control: condition Codes.&lt;/p&gt;
&lt;h3&gt;隐式修改: add等&lt;/h3&gt;
&lt;p&gt;Implicit Setting.&lt;/p&gt;
&lt;p&gt;算术逻辑运算，比如 $\mathtt{addq}$ 和减法等，可以在运算结束后&lt;strong&gt;自动设置&lt;/strong&gt;条件码寄存器。&lt;/p&gt;
&lt;p&gt;| 简称 | 名称          | 用途                                                         |
| ---- | ------------- | ------------------------------------------------------------ |
| CF   | Carry Flag    | 判断最高位是否进位，&lt;strong&gt;无符号数&lt;/strong&gt;溢出（第33位）               |
| ZF   | Zero Flag     | $\mathtt{if:t==0}$                                           |
| SF   | Sign Flag     | $\mathtt{if:t&amp;#x3C;0}$，只用于有符号数，自动检查符号位。          |
| OF   | Overflow Flag | &lt;strong&gt;有符号数&lt;/strong&gt;溢出（第32位）$\mathtt{(a&gt;0 ;&amp;#x26;&amp;#x26;; b&gt;0 ;&amp;#x26;&amp;#x26;; t&amp;#x3C;0) ;||;(a&amp;#x3C;0 ;&amp;#x26;&amp;#x26;; b&amp;#x3C;0 ;&amp;#x26;&amp;#x26;; t&gt;=0) }$ |&lt;/p&gt;
&lt;p&gt;注意：$\mathtt{leaq}$ 指令下的运算，不会使这四个寄存器改变，只能在算术逻辑运算时使用。&lt;/p&gt;
&lt;h3&gt;关于CF: sub/sbb&lt;/h3&gt;
&lt;p&gt;在 x86 机器中，减法操作将 CF 当作借位标志位（Borrow Flag），而不是溢出标志位。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当计算 $a-b$ 且 $a&amp;#x3C;b$ 时，CF=1，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;否则，CF=0。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有两种减法指令，分别是：SBB和SUB。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不带借位减法指令：$\mathtt{subq; b,; a}$，会计算 $a-b$，不会考虑借位，是较为常用的减法指令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;带借位减法指令：$\mathtt{sbbq; b,; a}$，会计算 $a-b-CF$，实际上可以用于超过标准字长的128位减法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;显式修改: Compare&lt;/h3&gt;
&lt;p&gt;Explicit Setting.
$$
\mathtt{cmpq; b,; a}
$$&lt;/p&gt;
&lt;p&gt;相当于计算 $a-b$ 的值，但是不存储到任何地方，只改变 condition codes.&lt;/p&gt;
&lt;h3&gt;显式修改: Test&lt;/h3&gt;
&lt;p&gt;Explicit Setting.
$$
\mathtt{testq; b,; a}
$$&lt;/p&gt;
&lt;p&gt;相当于计算 $a&amp;#x26;b$ 的值，但是不存储到任何地方，只改变 condition codes.&lt;/p&gt;
&lt;p&gt;特别地，当 $a$ 和 $b$ 中的一个值是 $mask$ 时，很有用，能且只能改变 ZF 和 SF：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 $\mathtt{ZF}$，当且仅当 $a&amp;#x26;b=0$ 时；&lt;/li&gt;
&lt;li&gt;设置 $\mathtt{SF}$，当且仅当 $a&amp;#x26;b$ 的最高位为 $1$ 时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;访问条件码&lt;/h3&gt;
&lt;p&gt;Reading Condition Codes.&lt;/p&gt;
&lt;p&gt;有以下操作，可以把条件码寄存器的值（及之间的运算），读取到 $\mathtt{dest}$ 寄存器上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine2-01.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;操作均只修改最低字节，改为为 $0$ 或者 $1$，而不改变原有的其他七个字节数据。但是，寄存器上，前七个字节的数据是未定义的。&lt;/p&gt;
&lt;p&gt;为了&lt;strong&gt;清除这七个字节的数据&lt;/strong&gt;，例如可以在 $\mathtt{setg;%al}$ 之后，用 $\mathtt{movzbl;%al, ;%eax}$ 指令，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$z$ 表示 zero-extend，即高位补零；&lt;/li&gt;
&lt;li&gt;$b$ 表示 byte，指源操作数是一个字节（8 位）的数据。&lt;/li&gt;
&lt;li&gt;$l$ 表示 long，在这上下文中通常表示目标寄存器是一个 long 类型数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其原理是：把一个字节扩展到八个字节。同时，这是一个32位指令，会把更高的七个字节清零。&lt;/p&gt;
&lt;h2&gt;条件分支&lt;/h2&gt;
&lt;p&gt;Conditional braches&lt;/p&gt;
&lt;h3&gt;Jumping 语句&lt;/h3&gt;
&lt;p&gt;根据控制码寄存器的值，选择是否跳转，并跳转到指定位置。有如下指令：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine2-02.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里判断的小于或者大于，看的是实际运算中 $b-a$ 的值，注意其与汇编代码中 $a,b$ 的顺序不同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;absdiff:
	cmpq %rsi, %rdi
	jle  .L4
	...
	ret
.L4:		# %rdi &amp;#x3C;= %rsi
	...
    ret
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional Move 语句&lt;/h3&gt;
&lt;p&gt;不改变流水线进程的一种条件跳转语句，不要求控制转移。在一些情况下，可以提高运行效率。&lt;/p&gt;
&lt;p&gt;这种翻译方式，&lt;strong&gt;会先同时计算两种情况&lt;/strong&gt;，然后根据条件码寄存器的值，选择其中一个结果，因此不适用于以下情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两种情况中，一种会导致异常：&lt;code&gt;val = p ? *p : 0&lt;/code&gt; 访问空指针的值；&lt;/li&gt;
&lt;li&gt;计算量很大，不适合同时计算两种情况；&lt;/li&gt;
&lt;li&gt;计算产生副作用，如 &lt;code&gt;val = p ? *p++ : 0&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，编译器在以上情况不会选择 Conditional Move，而是会选择 Jumping 语句。&lt;/p&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;p&gt;Loops.&lt;/p&gt;
&lt;h3&gt;do-while循环&lt;/h3&gt;
&lt;p&gt;是最基本的循环语句，其结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;loop:
	body-statement
	t = test
	    if (t)
	    	goto loop;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;While 循环&lt;/h3&gt;
&lt;p&gt;while 循环是 do-while 循环的变种，区别在于，首次之前会先执行 test。&lt;/p&gt;
&lt;h4&gt;Jump to middle&lt;/h4&gt;
&lt;p&gt;这是第一种翻译方法，利用 &lt;code&gt;goto&lt;/code&gt; 语句。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;	goto test;
loop:
	body-statement
test:
    t = test-expr
    if (t)
        goto loop;    
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Guarded-do&lt;/h4&gt;
&lt;p&gt;先用条件分支，然后转化为 do-while，优化程度更高。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;t = test-expr
if (!t)
    goto done
do:
    body-statement
    t = test-expr    
    if (t)
        goto do; 
done:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;For 循环&lt;/h3&gt;
&lt;p&gt;会被翻译为 while 循环。&lt;/p&gt;
&lt;h2&gt;Switch 语句&lt;/h2&gt;
&lt;p&gt;Switch Statements.&lt;/p&gt;
&lt;h3&gt;跳转表&lt;/h3&gt;
&lt;p&gt;Jump Table Structure.&lt;/p&gt;
&lt;p&gt;跳转表用数组存。&lt;/p&gt;
&lt;p&gt;通常情况下，数组的第 $0$ 位对应变量 $x$ 的最小值，第 $n$ 位对应最大值，而数组中的值则是对应的代码地址。&lt;/p&gt;
&lt;p&gt;对于稀疏的情况，编译器会使用 if-else 语句，而不是跳转表。&lt;/p&gt;
&lt;h3&gt;跳转步骤&lt;/h3&gt;
&lt;p&gt;通过跳转表来访问代码位置，是执行switch语句的关键步骤。如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine2-03.png&quot; alt=&quot;image-20250319222345932&quot;&gt;&lt;/p&gt;
&lt;p&gt;Multiple case labels，Fall through cases，Missing cases 是 switch 语句的三个特殊情况。&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Multiple case labels 在跳转表中，会有多个地址指向同一个代码块。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fall through cases 指的是不含 &lt;code&gt;break&lt;/code&gt; 语句的 case 语句，会顺序执行或直接跳转到下一个语句。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Missing cases 指的是没有在跳转表中的 case 语句，会&lt;strong&gt;指向 default 语句&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个例题：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine2-04.png&quot; alt=&quot;image-20250319224131348&quot;&gt;&lt;/p&gt;</content:encoded></item><item><title>程序的机器级表示 1</title><link>https://www.tonyyin0418.com/blog/csapp/machine1</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/machine1</guid><description>武汉大学2025春季学期 计算机系统基础 第三章 程序的机器级表示 1</description><pubDate>Sat, 08 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;程序的机器级表示 1&lt;/h2&gt;
&lt;p&gt;程序的机器级表示 1&lt;/p&gt;
&lt;h3&gt;History of Intel processors and architectures&lt;/h3&gt;
&lt;p&gt;本书只研究 x86-64 架构的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&gt; gcc -m64 hello.c
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C, Assembly, Machine code&lt;/h3&gt;
&lt;p&gt;代码格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assembly Code(汇编代码): A text representation of machine code.&lt;/li&gt;
&lt;li&gt;Object Code(目标代码): 和 Machine Code 差不多，只是少了 link 过程。&lt;/li&gt;
&lt;li&gt;Machine Code(机器代码): The byte-level programs that a processor executes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面这个由 c 语言转换到机器语言的图展示了这三种语言的关系：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine1-01.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Assembly code&lt;/h4&gt;
&lt;p&gt;下面的指令可以利用生成后缀名为 .s 的汇编文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -Og -S mstore.c # 生成汇编文件mstore.s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 -Og 是一个适用于调试的优化选项，确保生成的调试信息不会受到影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据类型：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;C语言&lt;/strong&gt; | &lt;strong&gt;Intel数据类型&lt;/strong&gt; | &lt;strong&gt;汇编后缀&lt;/strong&gt; | &lt;strong&gt;字节数&lt;/strong&gt; |
| :-------: | :---------------: | :----------: | :--------: |
|   char    |       Byte        |      b       |     1      |
|   short   |       Word        |      w       |     2      |
|    int    |    Double word    |      l       |     4      |
|   long    |     Quad word     |      q       |     8      |
|  char *   |     Quad word     |      q       |     8      |
|   float   | Single precision  |      s       |     4      |
|  double   | Double precision  |      l       |     8      |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;是在 Register / Memory 的数据上进行操作：&lt;strong&gt;运算、传送数据（Transfer data）、控制转移（Transfer control）&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Object Code&lt;/h4&gt;
&lt;p&gt;在 Assembler（汇编器）与 Linker（连接器）之间。&lt;/p&gt;
&lt;p&gt;汇编器的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.s文件汇编为.o文件&lt;/li&gt;
&lt;li&gt;指令的二进制编码，几乎是可执行的，只差链接过程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面的指令可以利用生成后缀名为 .s 的汇编文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -Og -c mstore.c # 生成目标代码文件mstore.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是二进制格式，无法直接查看，需要反汇编工具：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;objdump -d mstore.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来检查 object code，得到与汇编大致对应的代码，以分析指令序列。&lt;/p&gt;
&lt;h3&gt;Assembly Basics: Registers, operands, move&lt;/h3&gt;
&lt;p&gt;寄存器、操作数（运算对象）、数据传送指令。&lt;/p&gt;
&lt;h4&gt;传送数据&lt;/h4&gt;
&lt;p&gt;首先以 &lt;code&gt;movq&lt;/code&gt; 的一个示例来介绍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Instruction：&lt;/strong&gt; 指令，由操作码(Operation Code)和操作数(Operands)组成。比如下面是用于 Moving Data 的指令：&lt;/p&gt;
&lt;p&gt;$$
\mathtt{movq}; \mathsf{Source, Dest}
$$&lt;/p&gt;
&lt;p&gt;其中 $\mathtt{Source}$ 和 $\mathtt{Dest}$ 就是操作数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operand:&lt;/strong&gt; 操作数有以下类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Immediate&lt;/strong&gt;: const integer data. 比如：$0x400, $533，在前面加个 $ 即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register&lt;/strong&gt;: One of 16 integer registers. 共有十六个寄存器，例如 $\mathtt{%rax}$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Reference&lt;/strong&gt;: 8 consecutive bytes of memory at address given by register. 寄存器后的连续八个字节，记作 $\mathtt{(%rax)}$，加个括号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 $\mathtt{movq}$ 指令中，这三者都可以被组合起来，有如下用法：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./machine1-02.png&quot; alt=&quot;movq Operand Combinations&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意不存在 mem 到 mem 的操作。&lt;/p&gt;
&lt;h4&gt;完整寻址模式&lt;/h4&gt;
&lt;p&gt;$$
\begin{array}{rl}
\mathtt{D(Rb, Ri, S)} &amp;#x26; \mathtt{Mem[;Reg[Rb];+;S*Reg[Ri];+;D;]}
\end{array}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\mathtt{D}$：长偏移量，1、2、4 字节&lt;/li&gt;
&lt;li&gt;$\mathtt{Rb}$：基址寄存器，16个整型寄存器中的任意一个&lt;/li&gt;
&lt;li&gt;$\mathtt{Ri}$：索引寄存器，除 %rsp 外的任一寄存器&lt;/li&gt;
&lt;li&gt;$\mathtt{S}$：伸缩值，1，2，4 或 8&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样可以计算出一个地址，然后进行数据传送。&lt;/p&gt;
&lt;h3&gt;Arithmetic &amp;#x26; logical operations&lt;/h3&gt;
&lt;p&gt;算数与运算逻辑操作。&lt;/p&gt;
&lt;h4&gt;地址计算指令&lt;/h4&gt;
&lt;p&gt;利用寻址模式表达式，可以进行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算地址但不存储，&lt;/li&gt;
&lt;li&gt;计算形如 $x + k*y$ 的算数表达式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathtt{leaq;Src, ;Dst}
$$&lt;/p&gt;
&lt;p&gt;这里用到的就是第二种功能，计算算数表达式，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;long m12(long x) {
    return x*12;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换为汇编语言为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;leaq    (%rdi,%rdi,2), %rax
salq    $2, %rax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 $\mathtt{(%rdi, %rdi, 2)}$ 是寻址模式表达式，计算 $x + 2x = 3x$，&lt;/p&gt;
&lt;p&gt;然后 $\mathtt{salq}$ 是左移两位，即 $3x*2^2 = 12x$。&lt;/p&gt;
&lt;h4&gt;Arithmetic Operations&lt;/h4&gt;
&lt;p&gt;一些算数操作，包括二元运算和一元运算，这里不进行列举。&lt;/p&gt;
&lt;p&gt;在汇编层面，已经不区分有符号数和无符号数的运算，都是对二进制码的操作。&lt;/p&gt;
&lt;p&gt;二元运算中，以 $\mathtt{addq;Src,Dest}$ 为例，表示 $Dest = Dest + Src$，要注意参数的顺序。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;long arith(long x, long y, long z) {
    long t1 = x + y;
    long t2 = z + t1;
    long t3 = x + 4;
    long t4 = y * 48;
    long t5 = t3 + t4;
    long rval = t2 * t5;
    return rval;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其汇编代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;arith:
    leaq    (%rdi,%rsi), %rax   # t1 = x + y
    addq    %rdx, %rax          # t2 = z + t1
    leaq    (%rsi,%rsi,2), %rdx # for t4
    salq    $4, %rdx            # t4 = y * 48
    leaq    4(%rdi,%rdx), %rcx  # t5 = t3 + t4
    imulq   %rcx, %rax          # rval = t2 * t5
    ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里寄存器与参数的对应关系为：&lt;/p&gt;
&lt;p&gt;| 寄存器           | 参数         |
| ---------------- | ------------ |
| $\mathtt{%rdi}$ | $\mathtt{x}$ |
| $\mathtt{%rsi}$ | $\mathtt{y}$ |
| $\mathtt{%rdx}$ | $\mathtt{z}$ |
| $\mathtt{%rax}$ | $\mathtt{t1,t2,rval}$ |
| $\mathtt{%rdx}$ | $\mathtt{t4}$ |
| $\mathtt{%rcx}$ | $\mathtt{t4}$ |&lt;/p&gt;
&lt;p&gt;注意寄存器的重复使用，以及编译器没有显式计算 $t3$ 的值，以节省寄存器。&lt;/p&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded></item><item><title>计算机系统基础 Data Lab</title><link>https://www.tonyyin0418.com/blog/csapp/data-lab</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/data-lab</guid><description>武汉大学2025春季学期 计算机系统基础 Lab1 - Data Lab</description><pubDate>Tue, 04 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;对于运行在 Apple Silicon 芯片上的 MacOS 系统，可以参考 &lt;a href=&quot;https://www.tonyyin.top/blog/csapp_env&quot;&gt;这篇博文&lt;/a&gt; 进行配置环境。&lt;/p&gt;
&lt;h2&gt;puzzle列表&lt;/h2&gt;
&lt;p&gt;本次实验是对位操作级别的练习，题目包括：&lt;/p&gt;
&lt;p&gt;| 函数名称         | 题目描述                                           | 评分 | 最大操作次数 |
|----------------|------------------------------------------------|------|------------|
| bitOr(x, y)    | 只用 ~ 和 &amp;#x26; 实现 x | y                          | 1    |8|
| anyEvenBit(x)  | 如果任意偶数位为1，则返回1                      | 2    | 12         |
| rotateLeft(x, n)| 将 x 向左旋转 n 位（左移）                    | 3    | 25         |
| greatestBitPos(x)| 返回最高位的 1 所在的掩码                       | 4    | 70         |
| leastBitPos(x) | 返回最低位的 1 所在的掩码                       | 2    | 6          |
| subOK(x, y)    | 判断 x - y 是否不会溢出                       | 3    | 20         |
| satMul3(x)     | 计算 x * 3，并在溢出时饱和到 Tmin 或 Tmax      | 3    | 25         |
| divpwr2(x, n)  | 计算 x / (2 ^ n)，向零方向取整              | 2    | 15         |
| float_abs(uf)  | 返回浮点数的绝对值，如果是 NaN 则返回此浮点数    | 2    | 10         |
| float_i2f(x)   | 将整数 x 转换成浮点数表示                     | 4    | 30         |&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;bitOr&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：只用 &lt;code&gt;~&lt;/code&gt; 和 &lt;code&gt;&amp;#x26;&lt;/code&gt; 实现 &lt;code&gt;x | y&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：8&lt;/li&gt;
&lt;li&gt;评分：1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用了德摩根定律来实现按位或运算。
$$
x\text{ OR }y=\sim(\sim x\text{ AND } \sim y)
$$
代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int bitOr(int x, int y) {
	// x|y = ~(~x&amp;#x26;~y)
	return ~(~x&amp;#x26;~y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;anyEvenBit&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：如果任意偶数位为1，则返回1&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：12&lt;/li&gt;
&lt;li&gt;评分：2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个朴素的思路是：构建偶数位为 $1$，奇数位为 $0$ 的掩码 $\mathtt{0101\dots 010101}$，之后则容易解题。&lt;/p&gt;
&lt;p&gt;而在这个 Lab 中，要求可以用的最大常数值为 $\mathtt{0xFF}$，因此得到上述掩码的过程为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建掩码 $A\leftarrow \mathtt{0x55}$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A | (A &amp;#x3C;&amp;#x3C; 8)&lt;/code&gt;，得到 $\mathtt{0x5555}$，&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A | (A &amp;#x3C;&amp;#x3C; 16)&lt;/code&gt;，得到 $\mathtt{0x55555555}$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终通过逻辑非运算符 &lt;code&gt;!!&lt;/code&gt; 返回结果，以确保返回值为1或0。&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int anyEvenBit(int x) {
	// 0101 0101 ... 0101 = 0x5555555, 0x55(8 digits)
	int A = 0x55;
	A = A | (A &amp;#x3C;&amp;#x3C; 8);
	A = A | (A &amp;#x3C;&amp;#x3C; 16);
	return !!((x&amp;#x26;A));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;rotateLeft&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：将 x 向左旋转 n 位&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：25&lt;/li&gt;
&lt;li&gt;评分：3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将 x 向左移 $n$ 位，会丢失高位信息，因此需要记录最高 $n$ 位的信息。&lt;/p&gt;
&lt;p&gt;记录 $n$ 位信息用掩码 $\mathtt{11\dots1100000000}$ 进行按位与运算即可。&lt;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int rotateLeft(int x, int n) {
	int minus_n = (~n) + 1;
	int minus_one = (~0);
	int high_n_digits = (x&gt;&gt;(32+minus_n)) &amp;#x26; ((1&amp;#x3C;&amp;#x3C;n)+minus_one);
	// 这里貌似 (x&gt;&gt;(31+minus_n)&gt;&gt;1) 才不会UB，但这样可以省一个op
	return (x &amp;#x3C;&amp;#x3C; n) | high_n_digits;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;greatestBitPos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：返回最高位的1所在的掩码&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：70&lt;/li&gt;
&lt;li&gt;评分：4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原版的方法是：倍增求前导零长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int x_zero = (!x);
high_16 = (!(x &gt;&gt; 15)) &amp;#x3C;&amp;#x3C; 4; // val: 16 or 0
x = x &amp;#x3C;&amp;#x3C; high_16;
high_8 = (!(x &gt;&gt; 23)) &amp;#x3C;&amp;#x3C; 3; // val: 8 or 0
x = x &amp;#x3C;&amp;#x3C; high_8;
high_4 = (!(x &gt;&gt; 27)) &amp;#x3C;&amp;#x3C; 2; // val: 4 or 0
x = x &amp;#x3C;&amp;#x3C; high_4;
high_2 = (!(x &gt;&gt; 29)) &amp;#x3C;&amp;#x3C; 1; // val: 2 or 0  
x = x &amp;#x3C;&amp;#x3C; high_2;
high_1 = (!(x &gt;&gt; 30)); // val: 1 or 0
sign = !(x &gt;&gt; 31);
cnt = high_16 + high_8 + high_4 + high_2 + high_1 + sign;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于循环判断前 $2^k$ 位，是否全为 $0$，&lt;/p&gt;
&lt;p&gt;如果全为 $0$，就左移 $2^k$ 位，否则继续看 $2^{k-1}$ 位。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&quot;https://stackoverflow.com/questions/21413565/create-a-mask-that-marks-the-most-significant-set-bit-using-only-bitwise-operat&quot;&gt;stackoverflow&lt;/a&gt; 的方法，得到以下刷榜做法：&lt;/p&gt;
&lt;p&gt;为了找出最高位的1，采用了逻辑移位和逐步合并的策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先依次将 x 的高位信息合并，将所有 $1$ 填充到低位。&lt;/li&gt;
&lt;li&gt;根据代码来看，由于只有右移和按位或，高位的 $1$ 不可能丢失，同时低位会被填满，高位不受影响。&lt;/li&gt;
&lt;li&gt;最后，通过与 &lt;code&gt;(~x &gt;&gt; 1) ^ (1 &amp;#x3C;&amp;#x3C; 31)&lt;/code&gt;，得到最终的高位mask。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个方法很简洁，甚至不需要讨论正负：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int greatestBitPos(int x) {
	// x&amp;#x3C;0: 111111111
	// x=0: 000000000
	// x&gt;0: 000011111
	x |= x &gt;&gt; 1;
	x |= x &gt;&gt; 2;
	x |= x &gt;&gt; 4;
	x |= x &gt;&gt; 8;
	x |= x &gt;&gt; 16;
	return x &amp;#x26; ((~x&gt;&gt;1) ^ (1&amp;#x3C;&amp;#x3C;31));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;leastBitPos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：返回最低位的1所在的掩码&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：6&lt;/li&gt;
&lt;li&gt;评分：2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;利用了 &lt;code&gt;x &amp;#x26; (-x)&lt;/code&gt; 求得。负数的补码可以被用于快速找到最低位的1。&lt;/p&gt;
&lt;p&gt;举例：
$$
\begin{array}{r|c}
x &amp;#x26;  \mathtt{000101101000}\
\sim x &amp;#x26; \mathtt{111010010111}\
-x=\sim x+1 &amp;#x26; \mathtt{111010011000}\
x\text{ AND }(-x) &amp;#x26; \mathtt{000000001000}
\end{array}
$$
由于 &lt;code&gt;-x=(~x)+1&lt;/code&gt;，低位连续的零取反后变成连续的1，再加 $1$ 进位。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int leastBitPos(int x) {
	// lowbit, x &amp;#x26; (-x)
	return (~x+1) &amp;#x26; x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;subOK&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：判断 x - y 是否不会溢出&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：20&lt;/li&gt;
&lt;li&gt;评分：3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先计算 y 的相反数，然后将其与 x 相加，得到相减的结果。&lt;/p&gt;
&lt;p&gt;然后通过异或运算检查是否存在符号位变化，从而判断是否发生溢出。&lt;/p&gt;
&lt;p&gt;右移 $31$ 位，获取符号位，并返回布尔值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int subOK(int x, int y) {
	int minus_y = (~y+1);
	int result = x + minus_y;
	int overflow = (x ^ y) &amp;#x26; (x ^ result); // 只关注符号位
	return !(overflow &gt;&gt; 31);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;satMul3&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：计算 x * 3，并在溢出时饱和到 Tmin 或 Tmax&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：25&lt;/li&gt;
&lt;li&gt;评分：3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和上一题类似，注意到 $x\times 3$ 相当于两次加法，用&lt;strong&gt;异或&lt;/strong&gt;，同时判断这两次加法是否溢出。&lt;/p&gt;
&lt;p&gt;得到 &lt;code&gt;overflow = ( (x^x2)|(x2^x3) ) &gt;&gt; 31&lt;/code&gt; 的取值为：$\mathtt{0x0}$ 或 $\mathtt{0xFFFFFFFF}$.&lt;/p&gt;
&lt;p&gt;之后用 &lt;code&gt;~(sign ^ tmin)&lt;/code&gt; 巧妙得到正/负溢出后的饱和值：&lt;code&gt;tmin&lt;/code&gt; 或 &lt;code&gt;tmax&lt;/code&gt;，例如：
$$
\begin{array}{r|c}
\text{Tmin} &amp;#x26; \mathtt{100000000000}\
\
\text{sign_negative} &amp;#x26; \mathtt{111111111111}\
\text{sign XOR Tmin} &amp;#x26; \mathtt{011111111111}\
\
\text{sign_positive} &amp;#x26; \mathtt{000000000000}\
\text{sign XOR Tmin} &amp;#x26; \mathtt{100000000000}\&lt;/p&gt;
&lt;p&gt;\end{array}
$$
然后用 &lt;code&gt;overflow&lt;/code&gt; 判断是否溢出，返回对应答案即可。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;divpwr2&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：计算 x/(2^n)，向零方向取整&lt;/li&gt;
&lt;li&gt;允许的运算符：&lt;code&gt;!&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;&amp;#x26;&lt;/code&gt; &lt;code&gt;^&lt;/code&gt; &lt;code&gt;|&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最大操作次数：15&lt;/li&gt;
&lt;li&gt;评分：2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于要向零舍入，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正数：可以直接右移；&lt;/li&gt;
&lt;li&gt;负数：需要加偏移量 &lt;code&gt;bias&lt;/code&gt; 之后右移，其中 $\text{bias}=2^n-1$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了&lt;code&gt;(1 &amp;#x3C;&amp;#x3C; n) + (~0)&lt;/code&gt; 之外，还有一种用较少运算符计算 &lt;code&gt;bias&lt;/code&gt; 的方法，无需再判断正负。&lt;/p&gt;
&lt;p&gt;详细见代码，原理没有区别，利用&lt;strong&gt;异或&lt;/strong&gt;求得形如 $000011111$ 的掩码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int divpwr2(int x, int n) {
	/*
	下面原理与此相同，更巧妙地融合了sign，少2ops
	bias = (1 &amp;#x3C;&amp;#x3C; n) + (~0);
	sign_mask = (x &gt;&gt; 31);
	bias &amp;#x26;= sign_mask;
	*/
	int bias = (x &gt;&gt; 31);
	bias = bias ^ (bias &amp;#x3C;&amp;#x3C; n);
	x = x + bias; // 负数加bias
	return x &gt;&gt; n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;float_abs&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：返回浮点数的绝对值，如果是 NaN 则返回此浮点数&lt;/li&gt;
&lt;li&gt;允许的运算符：任何整数/无符号运算（包括 ||, &amp;#x26;&amp;#x26;），以及 if, while&lt;/li&gt;
&lt;li&gt;最大操作次数：10&lt;/li&gt;
&lt;li&gt;评分：2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从这题开始，不再限制运算符，因此灵活很多。&lt;/p&gt;
&lt;p&gt;首先，通过与掩码 &lt;code&gt;0x7FFFFFFF&lt;/code&gt; 进行按位与操作，清除符号位。&lt;/p&gt;
&lt;p&gt;接下来注意到，比 $\mathtt{0x7f800000,(0111;1111;1000;0000\cdots)}$ 大的数，都是 &lt;code&gt;NaN&lt;/code&gt;，判断即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;unsigned float_abs(unsigned uf) {
	unsigned ans = uf &amp;#x26; 0x7FFFFFFF;
	// 判断NaN的方法，可以简化为比0x7f800000大（0111 1111 0000 0000 ...）
	if(ans &gt; 0x7f800000) return uf; // NaN
	else return ans; // Otherwise
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;float_i2f&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目描述：将整数 x 转换成浮点数表示&lt;/li&gt;
&lt;li&gt;允许的运算符：任何整数/无符号运算（包括 ||, &amp;#x26;&amp;#x26;），以及 if, while&lt;/li&gt;
&lt;li&gt;最大操作次数：30&lt;/li&gt;
&lt;li&gt;评分：4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在实现浮点数转换的过程中，先判断输入 x 的特性，特别关注符号处理和特殊情况（如 0 和最小值）。接着统一成正数的情况，通过位操作找出 x 的指数和尾数。&lt;/p&gt;
&lt;p&gt;注意难点在于&lt;strong&gt;四舍五入的逻辑&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;unsigned float_i2f(int x) {
	// int 到 float 一定不会溢出，都是 Normalized
	// 取最高位的1，以下为frac，位置为exp
	unsigned ans = 0, exp, frac, pos, round;
	// 1. sign
	switch(x) {
		case 0x80000000: return 0xcf000000;
		case 0: return 0;
	}
	if(x &amp;#x3C; 0) {
		x = -x;
		ans = 0x80000000;
	}
	// 2. exp 找最高位的1
	pos = 31;
	while(!(x &gt;&gt; pos)) pos--; 
	exp = pos + 0x7f;
	// 3. frac
	x ^= (1 &amp;#x3C;&amp;#x3C; pos);
	x = x &amp;#x3C;&amp;#x3C; (31 - pos); // 为了舍入方便，先把有效位往左对齐，再移动回去
	frac = (x &gt;&gt; 8);
	round = x &amp;#x26; 0xff;
	if(round &gt; 0x80) frac++; // 四舍 六入
	else if(round == 0x80 &amp;#x26;&amp;#x26; frac &amp;#x26; 1) frac++; // 五成双
	if(frac &gt;&gt; 23) frac ^= (1 &amp;#x3C;&amp;#x3C; 23), exp++;
	ans = ans | (exp &amp;#x3C;&amp;#x3C; 23) | frac;
	return ans;
}
// 10101 = 1.0101 &amp;#x3C;&amp;#x3C; 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded></item><item><title>计算机系统基础 C Programming Lab</title><link>https://www.tonyyin0418.com/blog/csapp/c-programming-lab</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/csapp/c-programming-lab</guid><description>武汉大学2025春季学期 计算机系统基础 Lab0 - C Programming Lab</description><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;C Programming Lab 是计算机系统基础的第一个 lab，主要考察 C 语言编程能力，包括一些内存管理、用指针实现队列数据结构的方法。&lt;/p&gt;
&lt;p&gt;在解压&lt;code&gt;.tar&lt;/code&gt;压缩包，进入工作目录后，就可以在 x86 架构的 Linux 环境下完成此 lab 了。&lt;/p&gt;
&lt;p&gt;对于运行在 Apple Silicon 芯片上的 MacOS 系统，可以参考 &lt;a href=&quot;https://www.tonyyin.top/blog/csapp_env&quot;&gt;这篇博文&lt;/a&gt; 进行配置环境。&lt;/p&gt;
&lt;p&gt;另外，也可以考虑使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WSL2&lt;/li&gt;
&lt;li&gt;云服务器&lt;/li&gt;
&lt;li&gt;希冀平台提供的云服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;写前须知&lt;/h2&gt;
&lt;h3&gt;文件说明&lt;/h3&gt;
&lt;p&gt;只需要修改并提交这三个文件，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Makefile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue.h&lt;/code&gt;：加入想引入的新字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue.c&lt;/code&gt;：队列函数的实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他文件的用法详见 README 文档。&lt;/p&gt;
&lt;h3&gt;使用qtest&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;qtest&lt;/code&gt; 提供了一个命令解释器，可用来创建和操作队列。运行以下命令查看命令行选项列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./qtest -h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当您执行 &lt;code&gt;./qtest&lt;/code&gt; 后，也可以输入 &lt;code&gt;help&lt;/code&gt; 来查看可用命令列表。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./qtest

new
ih 3
ih 1
ih 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现错误，则会自动输出错误信息，供调试。&lt;/p&gt;
&lt;h3&gt;自动测评程序&lt;/h3&gt;
&lt;p&gt;在初步调试之后，下面的命令用来检查代码的正确性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里实际上是在 &lt;code&gt;make&lt;/code&gt; 之后运行了 &lt;code&gt;./driver.py&lt;/code&gt; 脚本，注意这是在 &lt;code&gt;Python2&lt;/code&gt; 标准下编写的。&lt;/p&gt;
&lt;p&gt;另外，这些本地测试点在 &lt;code&gt;/traces&lt;/code&gt; 目录下的 &lt;code&gt;.cmd&lt;/code&gt; 文件中，可供调试参考。&lt;/p&gt;
&lt;h2&gt;更新 Makefile&lt;/h2&gt;
&lt;p&gt;首先要修正压缩包中提供的 &lt;code&gt;Makefile&lt;/code&gt; 文件，根据 &lt;code&gt;README&lt;/code&gt;和 &lt;code&gt;Makefile&lt;/code&gt;中的提示可完成。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;qtest&lt;/code&gt; 的依赖项中添加 &lt;code&gt;queue.o&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;qtest: qtest.c report.c console.c harness.c queue.o
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;在链接过程中加入 &lt;code&gt;queue.o&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;$(CC) $(CFLAGS) -o qtest qtest.c report.c console.c harness.c queue.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要验证 &lt;code&gt;Makefile&lt;/code&gt; 是否正确，在终端运行 &lt;code&gt;make&lt;/code&gt;，如果正确，则应该得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -O0 -g -Wall -Werror -o qtest qtest.c report.c console.c harness.c queue.o
tar -cf handin.tar queue.c queue.h Makefile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似的结果，没有任何错误信息。&lt;/p&gt;
&lt;h2&gt;更新 queue.h&lt;/h2&gt;
&lt;p&gt;要在 &lt;code&gt;queue_t&lt;/code&gt; 类型中，声明 &lt;code&gt;tail&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt; 两个元素，原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于 &lt;code&gt;q_insert_tail&lt;/code&gt; 要求 $\mathcal{O}(1)$ 复杂度，不能遍历整个链表&lt;/li&gt;
&lt;li&gt;用于实现 &lt;code&gt;q_size&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef struct {
    list_ele_t *head;
    list_ele_t *tail;
    int size;
} queue_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;q_new&lt;/h2&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;检查 malloc 返回值是否为 NULL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：队列的 &lt;code&gt;size&lt;/code&gt; 设置为 0，&lt;code&gt;head&lt;/code&gt; 和 &lt;code&gt;tail&lt;/code&gt;指针都初始化为 NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;q_free&lt;/h2&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;
&lt;p&gt;遍历链表，并逐一释放元素，同时最后释放队列结构本身。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特殊处理 q 为 NULL 的情况（之后函数也一样）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;q_insert_head&lt;/h2&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;特殊处理 q 为 NULL 的情况&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;malloc&lt;/code&gt; 返回值是否为 NULL&lt;/li&gt;
&lt;li&gt;如果只有一个元素，要更新 tail 指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;q_insert_tail&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;功能&lt;/strong&gt;：尝试在队列头部插入一个新元素。&lt;/p&gt;
&lt;p&gt;跟上面没啥区别。&lt;/p&gt;
&lt;h2&gt;q_remove_head&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;功能&lt;/strong&gt;：从队列头部移除一个元素，并在移除成功时将值存储到 &lt;code&gt;vp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;易错点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特殊处理队列为空、q 为 NULL 的情况&lt;/li&gt;
&lt;li&gt;特殊处理 vp 为 NULL 的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;q_size&lt;/h2&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;特殊处理 q 为 NULL 的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;q_reverse&lt;/h2&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;实现这个功能无需双向链表&lt;/li&gt;
&lt;li&gt;注意设定 q-&gt;head-&gt;next = NULL，避免成环&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;评分结果&lt;/h2&gt;
&lt;p&gt;将工作目录的 &lt;code&gt;handle.tar&lt;/code&gt; 上传到希冀平台，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 进入目录
cd /mnt/cgshare

# 解压缩
tar xvf handin.tar

# 复制到桌面
cp Makefile queue.c queue.h ~/Desktop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交评测，得到以下结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---    Trace        Points
+++ TESTING trace trace-00-make    
---    trace-00-make    7/7
+++ TESTING trace trace-01-ops    
---    trace-01-ops    6/6
+++ TESTING trace trace-02-ops    
---    trace-02-ops    6/6
+++ TESTING trace trace-03-ops    
---    trace-03-ops    6/6
+++ TESTING trace trace-04-ops    
---    trace-04-ops    6/6
+++ TESTING trace trace-05-ops    
---    trace-05-ops    6/6
+++ TESTING trace trace-06-robust    
---    trace-06-robust    7/7
+++ TESTING trace trace-07-robust    
---    trace-07-robust    7/7
+++ TESTING trace trace-08-robust    
---    trace-08-robust    7/7
+++ TESTING trace trace-09-malloc    
---    trace-09-malloc    7/7
+++ TESTING trace trace-10-malloc    
---    trace-10-malloc    7/7
+++ TESTING trace trace-11-malloc    
---    trace-11-malloc    7/7
+++ TESTING trace trace-12-perf    
---    trace-12-perf    7/7
+++ TESTING trace trace-13-perf    
---    trace-13-perf    7/7
+++ TESTING trace trace-14-perf    
---    trace-14-perf    7/7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;</content:encoded></item><item><title>【CodeForces-1295F】Good Contest</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1295f</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1295f</guid><description>算法题解</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;数列 $a$ 在下列条件下随机生成。&lt;/p&gt;
&lt;p&gt;$a_i$ 是给定的区间 $[l_i, r_i]$ 内的正整数，等概率随机。&lt;/p&gt;
&lt;p&gt;求 $a_{1\cdots n}$ 单调不增的概率，对 $998244353$ 取模。&lt;/p&gt;
&lt;p&gt;$2\leq n\leq 50$，$0\leq l_i, r_i\leq 998244351$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;首先容易想到，概率可以由&lt;strong&gt;总方案数和单调不增的方案数&lt;/strong&gt;求得，其中总方案数为：
$$
A=\prod_{i=1}^n (r_i-l_i+1)
$$
只需求出单调不增的序列个数 $B$，则：
$$
p=\frac{B}{A}\bmod 998244353
$$
&lt;strong&gt;对于值域较小的情况，&lt;strong&gt;可以想到&lt;/strong&gt;设 $f_{i,j}$ 表示：选好了前 $i$ 个数且 $a_i=j$ 的情况下的方案数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;转移只需：
$$
f_{i,j}=\sum_{k\geq j} f_{i-1, k}
$$
但可惜本题中值域较大，考虑做优化。&lt;/p&gt;
&lt;p&gt;想到&lt;strong&gt;离散化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对区间端点进行离散化&lt;/strong&gt;，用 $p_i$ 表示所有端点坐标中数值第 $i$ 大的数，&lt;/p&gt;
&lt;p&gt;$a_i$ 表示左端点离散化后编号，$b_i$ 表示右端点离散化后编号。&lt;/p&gt;
&lt;p&gt;此时需要微调DP状态，但思路类似。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设 $f_{i, j}$ 表示选好了前 $i$ 个数且 $a_i\in [p_j, p_{j+1}-1]$，所形成单调不增数列的方案数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;转移时，&lt;strong&gt;由于进行了离散化，我们无法通过枚举确定 $a_{i-1}$ 与 $a_i$ 的大小关系&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;退而求其次，再枚举 $k$，保证 $a_{k+1},\cdots,a_i$ 的值都在 $[p_j, p_{j+1}-1]$ 内，计算这个长度为 $k$ 的区间内的方案数。&lt;/p&gt;
&lt;p&gt;记可能的取值个数 $L = p_{j+1}-p_j$.&lt;/p&gt;
&lt;p&gt;需要在其中选 $i-k$ 个数单调不增，方案数为：
$$
C_{i-k}=\binom{L+i-k-1}{i-k}
$$
这个组合数直接递推预处理就好了：
$$
C_x=\binom{L+x-1}{x}
\left{\begin{array}{lr}
L, &amp;#x26;x=1
\
C_{x-1}\cdot (L+x-1)\cdot \operatorname{Inv}(x), &amp;#x26;x&gt;1
\end{array}\right.
$$&lt;/p&gt;
&lt;p&gt;转移时，倒序枚举所有合法的 $k$，具体地：
$$
f_{i,j}=\sum_{j\in [a_k, b_k]\wedge k &amp;#x3C; i}(f_{k, j+1}\cdot C_{i-k})
$$
&lt;strong&gt;每个 $i$ 转移完之后，需要做前缀和，&lt;/strong&gt;
$$
f_{i,j}=\sum_{k\geq j} f_{i, k}
$$
这样对下一次转移才是对的。&lt;/p&gt;
&lt;p&gt;最终 $B=f_{n, 1}$，答案为：
$$
\operatorname{Ans}=\frac{B}{A} \bmod 998244353
$$&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;

const int MAXN = 60, mod = 998244353;
inline int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		x = x * x % mod;
		k &gt;&gt;= 1;
	}
	return ret;
}

int n, l[MAXN], r[MAXN], p[MAXN &amp;#x3C;&amp;#x3C; 1], N;
int f[MAXN][MAXN &amp;#x3C;&amp;#x3C; 1], C[MAXN];
signed main() {
	cin &gt;&gt; n;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		cin &gt;&gt; l[i] &gt;&gt; r[i];
		p[++N] = l[i], p[++N] = ++r[i];
	}
	sort(p + 1, p + N + 1);
	N = unique(p + 1, p + N + 1) - p - 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		l[i] = lower_bound(p + 1, p + N + 1, l[i]) - p;
		r[i] = lower_bound(p + 1, p + N + 1, r[i]) - p;
	}
	l[0] = 1, r[0] = N + 1;
	for(int i = 1; i &amp;#x3C;= N; i++) f[0][i] = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = l[i]; j &amp;#x3C; r[i]; j++) {
			int len = p[j + 1] - p[j];
			C[1] = len;
			for(int k = 2; k &amp;#x3C;= i; k++) 
                C[k] = C[k - 1] * (len + k - 1) % mod * power(k, mod - 2) % mod;
			for(int k = i - 1; k &gt;= 0; k--) {
				(f[i][j] += f[k][j + 1] * C[i - k] % mod) %= mod;
				if(j &amp;#x3C; l[k] || j &gt;= r[k]) break;
			}
		}
		for(int j = N - 1; j &gt; 0; j--) (f[i][j] += f[i][j + 1]) %= mod;
	}
	int ans = f[n][1];
	for(int i = 1; i &amp;#x3C;= n; i++)
		ans = ans * power(p[r[i]] - p[l[i]], mod - 2) % mod;
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces-149D】Coloring Brackets</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf149d</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf149d</guid><description>算法题解</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&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;相邻两个括号&lt;/strong&gt;：若都有颜色，要求它们的颜色互不相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;求符合条件的染色方案数，对 $10^9+7$ 取模。&lt;/p&gt;
&lt;p&gt;序列长度：$2\leq |s|\leq 700$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;区间DP.&lt;/p&gt;
&lt;p&gt;将颜色映射到三个数：
$$
\operatorname{Color}=\left{\begin{array}{ll}
0 &amp;#x26; \text{No Color}\
1 &amp;#x26; \text{Red}\
2 &amp;#x26; \text{Blue}\
\end{array}\right.
$$
设 $f[l][r][x][y]$ 为区间 $[l, r]$ 内，且 $\operatorname{Color}_l=x, \operatorname{Color}_r=y$ 的方案数。&lt;/p&gt;
&lt;h6&gt;下面分类讨论状态转移方式：&lt;/h6&gt;
&lt;h4&gt;情况一：$()$&lt;/h4&gt;
&lt;p&gt;若 $l+1=r$，则这段序列仅有一对左右括号，只能对其中一个染色。&lt;/p&gt;
&lt;p&gt;作为初始情况，可直接赋值：
$$
f[l][r][0][1]=f[l][r][0][2]=f[l][r][1][0]=f[l][r][2][0]=1
$$&lt;/p&gt;
&lt;h4&gt;情况二：$(\dots\dots)$&lt;/h4&gt;
&lt;p&gt;若 $l$ 与 $r$ 配成一对括号，则枚举 $\operatorname{Color}&lt;em&gt;{l+1}$ 和 $\operatorname{Color}&lt;/em&gt;{r-1}$，从 $f[l+1][r-1]$ 进行转移。&lt;/p&gt;
&lt;p&gt;$$
f[l][r][x][y]=\sum_{i=0\vee i\neq x},\sum_{j=0\vee j\neq y} f[l+1][r-1][i][j]
$$&lt;/p&gt;
&lt;h4&gt;情况三：$(\dots\dots) \dots(\dots)$&lt;/h4&gt;
&lt;p&gt;要求 $l$ 是左括号，$r$ 是右括号。设 $\operatorname{R}[i]$ 表示与 $i$ 配对的右括号。&lt;/p&gt;
&lt;p&gt;用 $f[l][\operatorname{R}[l]]\times f[\operatorname{R[l]+1}][r]$ 进行转移，具体过程要&lt;strong&gt;枚举四个颜色&lt;/strong&gt;，就不做形式化表达了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 0; i &amp;#x3C;= 2; i++) {
    for(int j = 0; j &amp;#x3C;= 2; j++) {
        for(int p = 0; p &amp;#x3C;= 2; p++) {
            for(int q = 0; q &amp;#x3C;= 2; q++) {
                if((j == 1 &amp;#x26;&amp;#x26; p == 1) || (j == 2 &amp;#x26;&amp;#x26; p == 2)) continue;
                (f[l][r][i][q] += (f[l][R[l]][i][j] * f[R[l] + 1][r][p][q]) % mod) %= mod;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【Luogu-P1156】垃圾陷阱</title><link>https://www.tonyyin0418.com/blog/oi-solution/p1156</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p1156</guid><description>算法题解</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定深井的深度 $D$ 和垃圾的数量 $G$.&lt;/p&gt;
&lt;p&gt;XLL想把垃圾堆起来，等到堆得与井同样高时，她就能逃出井外了。&lt;/p&gt;
&lt;p&gt;XLL可以通过吃垃圾来维持自己的生命。&lt;/p&gt;
&lt;p&gt;每个垃圾都可以用来吃或堆放，并且堆放垃圾不用花费XLL的时间。&lt;/p&gt;
&lt;p&gt;给定每个垃圾扔下的时间 $t_i$，高度 $h_i$，可供维持 $f_i$ 小时的生命。&lt;/p&gt;
&lt;p&gt;XLL在 $0$ 时刻位于井底，要保证任何时刻的生命储备时长（生命值） $&gt;0$，初始时生命值为 $10$ 小时。&lt;/p&gt;
&lt;p&gt;求最早何时爬出，或最长存活多久。&lt;/p&gt;
&lt;p&gt;$2\leq D, G\leq 100$，$1\leq t\leq 1000$，$1\leq h\leq 25$，$1\leq f \leq 30$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;背包。&lt;/p&gt;
&lt;p&gt;井 $\Rightarrow$ 背包大小，垃圾高度 $\Rightarrow$ 物品重量，维持生命的时长 $\Rightarrow$ 价值。&lt;/p&gt;
&lt;p&gt;对物品按 $t_i$ 升序排序，顺序加入背包。&lt;/p&gt;
&lt;p&gt;设 $g[i][j]$ 表示处理完前 $i$ 件物品后，高度为 $j$ 时的&lt;strong&gt;最大生命值&lt;/strong&gt;，有转移：
$$
g_{i, j} = \max\left{\begin{array}{l}g_{i - 1, j} + f[i]\g_{i-1, j-h[i]}\end{array}\right.
$$
&lt;strong&gt;普通的 $0/1$ 背包&lt;/strong&gt;即可解决。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P3047】[USACO12FEB]Nearby Cows G</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3047</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3047</guid><description>算法题解</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个点的数，点带权。&lt;strong&gt;对于每个节点 $i$，求出距离 $i$ 不超过 $k$ 的所有节点的权值和。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^5$，$1\leq k\leq 20$，$0\leq c_i\leq 1000$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;考虑树形DP。&lt;/p&gt;
&lt;p&gt;先任意钦定 $\rm{root}$.&lt;/p&gt;
&lt;p&gt;设 $f_{i, j}$ 表示在 $i$ 的子树中，距离 $i$ 小于等于 $j$ 的节点的权值和。&lt;/p&gt;
&lt;p&gt;可以用一次 $\rm {DFS}$ 求得 $f$，转移如下：
$$
f_{x, i}=c_x+\sum_{v} f_{v, i-1}
$$
设 $g_{i, j}$ 表示所有距离 $i$ 距离小于等于 $j$ 的节点的权值和，所以 $\operatorname{Ans}&lt;em&gt;i = g&lt;/em&gt;{i,k}$.&lt;/p&gt;
&lt;p&gt;再用一次 $\rm{DFS}$ 求得 $g$，转移如下：
$$
g_{x,i} = \left{\begin{array}{lr}
f_{x, i}+\sum\limits_{v}(g[\operatorname{fa}][i-1]-f_{x, i-2}), &amp;#x26;x=1\
f_{x, i}, &amp;#x26;x\neq 1
\end{array}\right.
$$
注意当 $i=1$ 时，应忽略 $f_{x, i-2}$ 项，否则会数组越界。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e5 + 10, MAXK = 25;
int n, k, c[MAXN];
vector&amp;#x3C;int&gt; g[MAXN];
int f[MAXN][MAXK];
void dfs1(int x, int fa) {
	for(int i = 0; i &amp;#x3C;= k; i++) f[x][i] = c[x];
	for(int v : g[x]) {
		if(v == fa) continue;
		dfs1(v, x);
		for(int i = 1; i &amp;#x3C;= k; i++) f[x][i] += f[v][i - 1];
	}
}
void dfs2(int x, int fa) {
	if(x != 1) {
		for(int i = k; i &gt;= 2; i--)
			f[x][i] += f[fa][i - 1] - f[x][i - 2];
		f[x][1] += f[fa][0];
	}
	for(int v : g[x]) {
		if(v == fa) continue;
		dfs2(v, x);
	}
}
int main() {
	n = read(), k = read();
	for(int i = 1; i &amp;#x3C; n; i++) {
		int u = read(), v = read();
		g[u].push_back(v), g[v].push_back(u);
	}
	for(int i = 1; i &amp;#x3C;= n; i++) c[i] = read();
	dfs1(1, 0); dfs2(1, 0);
	for(int i = 1; i &amp;#x3C;= n; i++) cout &amp;#x3C;&amp;#x3C; f[i][k] &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3959】[NOIP2017 提高组] 宝藏</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3959</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3959</guid><description>内含详细的位运算技巧和位运算枚举子集的方法</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;位运算技巧&lt;/h2&gt;
&lt;p&gt;|            位运算             |           功能            |                示例                |
| :---------------------------: | :-----------------------: | :--------------------------------: |
|        $\texttt{x&gt;&gt;k}$        |      去掉最后 $k$ 位      |    $101101\rightarrow 101, k=3$    |
|        $\texttt{x&amp;#x3C;&amp;#x3C;k}$        |    在最后加 $k$ 个 $0$    | $101101\rightarrow 101101000, k=3$ |
|       $\texttt{x&amp;#x3C;&amp;#x3C;1|1}$       |     在最后加一个 $1$      |    $101101\rightarrow 1011011$     |
|                               |                           |                                    |
|        $\texttt{x|1}$         |    把第 $1$ 位变成 $1$    |     $101100\rightarrow 101101$     |
|    $\texttt{x|(1&amp;#x3C;&amp;#x3C;(k-1))}$    |    把第 $k$ 位变成 $1$    |  $101001\rightarrow 101101, k=3$   |
|    $\texttt{x|((1&amp;#x3C;&amp;#x3C;k)-1)}$    |    把末 $k$ 位变成 $1$    |  $101001\rightarrow 101111, k=4$   |
|                               |                           |                                    |
|      $\texttt{x&amp;#x26;(-2)}$       |    把第 $1$ 位变成 $0$    |    $1010111\rightarrow 1010110$    |
| $\texttt{x &amp;#x26; (~(1&amp;#x3C;&amp;#x3C;(k-1)))}$ |    把第 $k$ 位变成 $0$    | $1010110\rightarrow 1000110, k=5$  |
|    $\texttt{x&amp;#x26;(-(1&amp;#x3C;&amp;#x3C;k))}$    |    把末 $k$ 位变成 $0$    | $1010110\rightarrow 1000000, k=5$  |
|                               |                           |                                    |
|     $\texttt{x ^{} 1} $      |      把第 $1$ 位取反      |     $101101\rightarrow 101100$     |
| $\texttt{x ^{} (1&amp;#x3C;&amp;#x3C;(k-1))}$  |      把第 $k$ 位取反      |  $101001\rightarrow 101101, k=3$   |
| $\texttt{x ^{} ((1&amp;#x3C;&amp;#x3C;k)-1)}$  |      把末 $k$ 位取反      | $1101101\rightarrow 1100010, k=4$  |
|                               |                           |                                    |
|        $\texttt{x&amp;#x26;7}$        |        取末 $3$ 位        |      $1101101\rightarrow 101$      |
|   $\texttt{x&amp;#x26;(1&amp;#x3C;&amp;#x3C;(k-1))}$    |        取末 $k$ 位        |  $1101101\rightarrow 01101, k=3$   |
|   $\texttt{(x&gt;&gt;(k-1))&amp;#x26;1}$    |        取第 $k$ 位        |    $1101101\rightarrow 1, k=4$     |
|                               |                           |                                    |
|      $\texttt{x&amp;#x26;(x+1)}$      | 把末尾连续的 $1$ 变成 $0$ |  $100101111\rightarrow 100100000$  |
|      $\texttt{x|(x+1)}$       | 把右边第一个 $0$ 变成 $1$ |  $100101111\rightarrow 100111111$  |
|      $\texttt{x|(x-1)}$       | 把末尾连续的 $0$ 变成 $1$ |  $100101000\rightarrow 100101111$  |
| $\texttt{(x ^{} (x+1))&gt;&gt;1}$  |     取末尾连续的 $1$      |    $100101111\rightarrow 1111$     |
|      $\texttt{x&amp;#x26;(-x)}$       | 取右边第一个 $1$ 及其右侧 |    $100101000\rightarrow 1000$     |&lt;/p&gt;
&lt;h2&gt;枚举子集、子集的子集&lt;/h2&gt;
&lt;h3&gt;枚举子集&lt;/h3&gt;
&lt;p&gt;用 $T$ 表示 $S$ 的子集，则一种枚举子集的方式是：
$$
\begin{array}{ll}
1&amp;#x26; \textbf{for }( \textbf{int } \text{T = S; T; T=(T - 1) &amp;#x26; S)}\
2&amp;#x26; \qquad \text{Do Something (T is a nonempty subset of S)}
\end{array}
$$
单次枚举 $T$ 的复杂度是 $\mathcal{O}(2^{|S|})$ 的。&lt;/p&gt;
&lt;h3&gt;枚举子集的子集&lt;/h3&gt;
&lt;p&gt;若要枚举 $S$ 的所有&lt;strong&gt;子集的子集&lt;/strong&gt;，可以证明复杂度为 $\mathcal{O}(3^n)$.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;组合意义天地灭，代数推导保平安。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;复杂度为：
$$
\begin{aligned}
\sum_{T\subseteq S}2^{|T|}=\sum_{i=1}^n\binom{n}{i} 2^i&amp;#x26;=\sum_{i=1}^n\binom{n}{i}2^i1^{n-i}\
&amp;#x26;=(1+2)^n-1\Rightarrow \mathcal{O}(3^n)
\end{aligned}
$$
证毕，参考 &lt;a href=&quot;https://www.cnblogs.com/CDOI-24374/p/15876755.html&quot;&gt;枚举子集为什么是 O(3^n) 的 - Jijidawang - 博客园&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;参与考古挖掘的小明得到了一份藏宝图，藏宝图上标出了 $n$ 个深埋在地下的宝藏屋， 也给出了这 $n$ 个宝藏屋之间可供开发的 $m$ 条道路和它们的长度。&lt;/p&gt;
&lt;p&gt;小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是，每个宝藏屋距离地面都很远，也就是说，从地面打通一条到某个宝藏屋的道路是很困难的，而开发宝藏屋之间的道路则相对容易很多。&lt;/p&gt;
&lt;p&gt;小明的决心感动了考古挖掘的赞助商，赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道，通往哪个宝藏屋则由小明来决定。&lt;/p&gt;
&lt;p&gt;在此基础上，小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路，小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外，小明不想开发无用道路，即两个已经被挖掘过的宝藏屋之间的道路无需再开发。&lt;/p&gt;
&lt;p&gt;新开发一条道路的代价是 $\mathrm{L} \times \mathrm{K}$。其中 $L$ 代表这条道路的长度，$K$ 代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量（包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋） 。&lt;/p&gt;
&lt;p&gt;请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路，使得工程总代价最小，并输出这个最小值。&lt;/p&gt;
&lt;p&gt;对于 $ 100%$ 的数据： $1 \le n \le 12$，$0 \le m \le 10^3$，$v \le  5\times 10^5$。&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;状压DP。&lt;/p&gt;
&lt;p&gt;找个&lt;strong&gt;生成树&lt;/strong&gt;，代价和最小。&lt;/p&gt;
&lt;p&gt;设 $f_{S, i}$ 表示当前包含的点集是 $S$，树高是 $i$，此时的最小代价和。&lt;/p&gt;
&lt;p&gt;则：
$$
f_{S, i}=\min_{T\subseteq S} {f_{T, i-1}+w(T, S)}
$$
其中 $w(T, S)$ 代表从 $T$ 变到 $S$ 的代价，预处理。&lt;/p&gt;
&lt;p&gt;预处理时，先找新增加的点，再找从 $T$ 连到这个点的最小边权，求和即可。顺便判断不合法的 $(T, S)$ 组合。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P4395】[BOI2003]Gem 气垫车</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4395</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4395</guid><description>内含详细的位运算技巧和位运算枚举子集的方法</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给出一棵 $N$ 个点的树，要求你为树上的结点标上权值，权值可以是任意的&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;p&gt;$1\leq N\leq 10000$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;只用 $1/2$ 染，是&lt;strong&gt;错误&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;反例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/10/example.svg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;结论：&lt;strong&gt;用 $\log n$ 种颜色即可&lt;/strong&gt;。于是树上背包，容易通过。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P5322】[BJOI2019]排兵布阵</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5322</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5322</guid><description>内含详细的位运算技巧和位运算枚举子集的方法</description><pubDate>Fri, 28 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;一个游戏，有 $n$ 座城堡，每局对战有两名玩家，每名玩家有 $m$ 个士兵，每人可以向第 $i$ 个城堡派遣 $a_i$ 名士兵，士兵总数满足：$a_1+a_2+\cdots+a_n\leq m$.&lt;/p&gt;
&lt;p&gt;二人争夺这些城堡，如果一名玩家向城堡 $i$ 派遣的士兵数&lt;strong&gt;严格大于&lt;/strong&gt;对手士兵数的两倍，那么这名玩家占领了这座城堡，获得 $i$ 分。&lt;/p&gt;
&lt;p&gt;现给定 $s$ 名玩家及其各自的派兵策略，你需要选定一个固定的派兵策略，最大化你在 $s$ 场游戏中的总得分。&lt;/p&gt;
&lt;p&gt;$1\leq s\leq 100, 1\leq n\leq 100, 1\leq m\leq 20000$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;考虑DP。&lt;/p&gt;
&lt;p&gt;设 $f_{i,j}$ 表示：对于前 $i$ 个城堡共派出 $j$ 个士兵；设 $a_{i,j}$ 表示：在城堡 $i$，玩家 $j$ 的出兵数量。&lt;/p&gt;
&lt;p&gt;对每个城堡，把各个玩家在该城堡的出兵数量，按升序排序。&lt;/p&gt;
&lt;p&gt;这样一来，若你在城堡 $i$ 的出兵数量大于 $2\times a_{i,j}$，则你可以在城堡 $i$ 获得 $i\times j$ 的得分。&lt;/p&gt;
&lt;p&gt;于是对每个赢得城堡 $i$，枚举要战胜前 $k$ 个玩家，分别转移即可：
$$
f[i][j]=\max_{k=1}^s
\left{
\begin{array}{lr}
0, &amp;#x26;j-a_{i,k}\times 2 - 1 &amp;#x3C; 0\
f[i][j-a_{i, k}\times2-1]+k\cdot i, &amp;#x26;j-a_{i,k}\times2-1\geq 0
\end{array}
\right.
$$
属于&lt;strong&gt;分组背包&lt;/strong&gt;模型。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 108, MAXM = 20008, MAXS = 108;
int s, n, m;
int dp[MAXM], a[MAXN][MAXS];//a[i][j]表示第i个城堡，第j个玩家
int main() {
	scanf(&quot;%d%d%d&quot;, &amp;#x26;s, &amp;#x26;n, &amp;#x26;m);
	for(int i = 1; i &amp;#x3C;= s; i++)
		for(int j = 1; j &amp;#x3C;= n; j++)
			scanf(&quot;%d&quot;, &amp;#x26;a[j][i]);
	for(int i = 1; i &amp;#x3C;= n; i++)
		sort(a[i] + 1, a[i] + s + 1);
	for(int k = 1; k &amp;#x3C;= n; k++) {
		for(int i = m; i &gt; 0; i--) {
			for(int j = 1; j &amp;#x3C;= s; j++) {
				if(i &gt;= a[k][j] * 2 + 1) {
					dp[i] = max(dp[i], dp[i - a[k][j] * 2 - 1] + k * j);
				}
			}
		}
	}
	cout &amp;#x3C;&amp;#x3C; dp[m] &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>主定理</title><link>https://www.tonyyin0418.com/blog/oi-notes/main-theory</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/main-theory</guid><description>CSP初赛复习</description><pubDate>Fri, 23 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;主定理&lt;/h2&gt;
&lt;p&gt;将一个规模为 $n$ 的问题，分治为 $a$ 个 $\lfloor\dfrac{n}{b}\rfloor$ 的子问题，每次带来的额外计算为 $\mathcal{O}(n^d)$，可得到以下关系式：&lt;/p&gt;
&lt;p&gt;$$
T(n)=aT(\lceil\dfrac{n}{b}\rceil)+\mathcal{O}(n^d)
$$&lt;/p&gt;
&lt;p&gt;$$
(\text{for,constants,}a&gt;0,b&gt;1,d\ge0),\text{then}:
$$&lt;/p&gt;
&lt;p&gt;$$
T(n) = \begin{cases}
\mathcal{O}{(n^d)}, &amp;#x26; \quad d&gt;\operatorname{log}_ba\
\mathcal{O}{(n^d\operatorname{log}n)}, &amp;#x26; \quad d=\operatorname{log}_ba\
\mathcal{O}{(n^{\operatorname{log}_ba})}, &amp;#x26; \quad d&amp;#x3C;\operatorname{log}_ba\
\end{cases}
$$&lt;/p&gt;</content:encoded></item><item><title>高一生物暑假作业 解析</title><link>https://www.tonyyin0418.com/blog/high-school/biology-sol</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/high-school/biology-sol</guid><description>生物暑假作业的解析，每节选五道题</description><pubDate>Sun, 28 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;物质运输&amp;#x26;细胞代谢&lt;/h1&gt;
&lt;h2&gt;2.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;影响被动运输速度的是膜两侧浓度差，&lt;strong&gt;随着M浓度的增加，吸收速率不变&lt;/strong&gt;，故A错误。&lt;/p&gt;
&lt;p&gt;B&amp;#x26;D错误。影响主动运输速度的有载体蛋白数量和能量。通入空气后，促进细胞有氧呼吸，使&lt;strong&gt;细胞有更多能量，但吸收速率不变&lt;/strong&gt;，故BD错误。&lt;/p&gt;
&lt;p&gt;由题意，M的吸收方式是主动运输，出现实验现象的原因为&lt;strong&gt;载体蛋白饱和&lt;/strong&gt;，故C正确。&lt;/p&gt;
&lt;p&gt;【考点】物质运输方式、物质运输速率的影响因素&lt;/p&gt;
&lt;h2&gt;8．&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【相关知识】&lt;/p&gt;
&lt;p&gt;酵母菌有氧呼吸过程：&lt;/p&gt;
&lt;p&gt;$$
\begin{array}{c}
\mathrm{C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6\xrightarrow{\text{酶}},,2\text{丙酮酸}+4[\mathrm{H]}+\mathrm{ATP}\2\text{丙酮酸}+6\mathrm{H}_2\mathrm{O}\xrightarrow{\text{酶}},,6\mathrm{CO}_2+20[\mathrm{H]}+\mathrm{ATP}\24[\mathrm{H]}+6\mathrm{O}_2\xrightarrow{\text{酶}},12\mathrm{H}_2\mathrm{O}+\mathrm{ATP}
\end{array}$$&lt;/p&gt;
&lt;p&gt;有氧呼吸总方程式：&lt;/p&gt;
&lt;p&gt;$$\begin{array}{c} \mathrm{C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6+6\mathrm{H}_2\mathrm{O}+6\mathrm{O}_2\xrightarrow{\text{酶}},6\mathrm{CO}_2+12\mathrm{H}_2\mathrm{O}+\mathrm{ATP}\end{array}$$&lt;/p&gt;
&lt;p&gt;酵母菌无氧呼吸过程：&lt;/p&gt;
&lt;p&gt;$$\begin{array}{c}\mathrm{C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6\xrightarrow{\text{酶}},,2\text{丙酮酸}+4[\mathrm{H]}+\mathrm{ATP}\2\text{丙酮酸}+4[\mathrm{H]}\xrightarrow{\text{酶}},,2\mathrm{C}_2\mathrm{H}_5\text{OH(酒精)}+2\mathrm{CO}_2
\end{array}$$&lt;/p&gt;
&lt;p&gt;无氧呼吸总方程式：&lt;/p&gt;
&lt;p&gt;$$\mathrm{C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6\xrightarrow{\text{酶}},,2\mathrm{C}_2\mathrm{H}_5\text{OH(酒精)}+2\mathrm{CO}_2+\mathrm{ATP}$$&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由无氧呼吸总方程式可知，氧浓度为a时，生成物中&lt;strong&gt;酒精与$\mathrm{CO}_2$物质的量相同&lt;/strong&gt;，只有无氧呼吸，故A正确。&lt;/p&gt;
&lt;p&gt;氧浓度为b时，酵母菌同时进行无氧呼吸和有氧呼吸。氧浓度为d时，产生酒精的量为0，只进行有氧呼吸。b与d呼吸方式不同，故B正确。&lt;/p&gt;
&lt;p&gt;氧浓度为c时，利用总方程式定量计算：无氧呼吸产生$6\mathrm{mol}$酒精，同时产生$6\mathrm{mol}$的$\mathrm{CO}_2$，消耗$3\mathrm{mol}$葡萄糖。有氧呼吸产生$(15-6)=9\mathrm{mol}$的$\mathrm{CO}_2$，消耗$9\div 6=1.5\mathrm{mol}$葡萄糖。所以用于酒精发酵的葡萄糖占比为：$\frac{3}{3+1.5}=\frac{2}{3}$，C错误。&lt;/p&gt;
&lt;p&gt;由呼吸过程可知，两种呼吸方式都产生[H]和ATP，D正确。&lt;/p&gt;
&lt;p&gt;【考点】细胞呼吸方式，细胞呼吸的定量计算&lt;/p&gt;
&lt;h2&gt;9.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;在$\mathrm{O}_2$浓度为$3%$时，$\mathrm{O}_2$吸收量小于$\mathrm{CO}_2$吸收量，所以苹果细胞既进行无氧呼吸也进行有氧呼吸。若只有有氧呼吸，则吸收量相等；若只有无氧呼吸，则$\mathrm{O}_2$吸收量为$0$，A正确。&lt;/p&gt;
&lt;p&gt;在$\mathrm{O}_2$浓度为$3%$时，有氧呼吸与无氧呼吸同时进行。利用方程式定量计算：有氧呼吸共消耗$0.3\mathrm{mol O}_2$，消耗 $0.05\mathrm{mol C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6$；则无氧呼吸的$\mathrm{CO}_2$释放量为$0.5-0.3=0.2\mathrm{mol}$，消耗$0.1\mathrm{mol C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6$.所以在1小时内共消耗$0.05+0.1=0.15\mathrm{mol}$葡萄糖，B正确。&lt;/p&gt;
&lt;p&gt;适宜贮藏苹果的环境，指有机物分解速率最低时的$0.05+0.1=0.15\mathrm{mol}$浓度。浓度为$5%$时，葡萄糖分解速率为$\frac{0.35}{6}+\frac{0.4-0.35}{2}=\frac{1}{12}=0.0833$，是表中最低数据，C正确。&lt;/p&gt;
&lt;p&gt;在$\mathrm{O}_2$浓度为$5%$时，有氧呼吸速率一定不是表中最弱。$\mathrm{O}_2$浓度为$0$时，有氧呼吸强度最弱，D错误。&lt;/p&gt;
&lt;p&gt;【考点】细胞光合作用，光合作用的定量计算&lt;/p&gt;
&lt;h2&gt;11.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【相关知识】&lt;/p&gt;
&lt;p&gt;**光补偿点：**在这个光照条件下，光合作用与呼吸作用速率相等，有机物的形成与消耗相等，不能积累干物质。&lt;/p&gt;
&lt;p&gt;**光饱和点：**在这个光照条件时光合作用速率最大，并且继续增加光照强度，光合作用速率不再增加。&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;图中横轴为光照强度。a点光照强度为0，表示&lt;strong&gt;细胞呼吸速率&lt;/strong&gt;；b点的光合作用和呼吸作用强度相等，表示&lt;strong&gt;光补偿点&lt;/strong&gt;；c点表示光合作用强度最大时（&lt;strong&gt;光饱和点&lt;/strong&gt;），对应的光照强度。&lt;/p&gt;
&lt;p&gt;由题意，温度从30℃降为25℃后，呼吸作用不再是最适温度，呼吸作用速率降低。反之，光合速率、最大光合速率都增加。&lt;/p&gt;
&lt;p&gt;呼吸作用速率降低，故a点上移。&lt;/p&gt;
&lt;p&gt;呼吸作用速率降低，光合作用速率增加。由于横轴为光照强度，&lt;strong&gt;为了使光合速率等于呼吸速率，需要降低光照强度，使b点左移&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在光合作用最适温度下，最大光合速率增加，光饱和点向右上方移动，故c点右移。&lt;/p&gt;
&lt;p&gt;故C正确，ABD错误。&lt;/p&gt;
&lt;p&gt;【考点】光合作用与呼吸作用的图像分析&lt;/p&gt;
&lt;h2&gt;14.&lt;/h2&gt;
&lt;p&gt;【答案】&lt;/p&gt;
&lt;p&gt;（1）$\mathrm{O}_2$，$\mathrm{CO}_2$&lt;/p&gt;
&lt;p&gt;（2）[b]，叶绿体基质，活跃的化学能 $\rightarrow $ 稳定的化学能，$\mathrm{CO}_2$ 的固定&lt;/p&gt;
&lt;p&gt;（3）C点，光照强度，无法确定&lt;/p&gt;
&lt;p&gt;（4）19时&lt;/p&gt;
&lt;p&gt;（5）588，大于，77&lt;/p&gt;
&lt;p&gt;（6）左&lt;/p&gt;
&lt;p&gt;【相关知识】&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/08/630b73f4512d9.jpg&quot; alt=&quot;image-20220719102521553&quot;&gt;&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;（1）提供$\mathrm{H}_2\mathrm{O}$，首先影响水的光解，使&lt;strong&gt;氧气&lt;/strong&gt;有放射性。生长一段时间后，带有放射性的氧气供线粒体进行呼吸作用，产生有放射性的&lt;strong&gt;二氧化碳&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;（2）光合作用暗反应场所为叶绿体基质，对应图乙中的b；暗反应将ATP中活跃的化学能储存到糖类中，使&lt;strong&gt;活跃的化学能转化为稳定的化学能&lt;/strong&gt;；暗反应分为$\mathbf{CO}_2$&lt;strong&gt;的固定&lt;/strong&gt;与$\mathbf{C}_3$&lt;strong&gt;的还原&lt;/strong&gt;两个部分。&lt;/p&gt;
&lt;p&gt;（3）&lt;/p&gt;
&lt;p&gt;图乙中：由叶绿体向线粒体提供氧气，所以&lt;strong&gt;光合作用强度大于呼吸作用强度&lt;/strong&gt;，与C点状态相同。&lt;/p&gt;
&lt;p&gt;丙图中，A-C段的光合速率随光照强度上升，所以主要限制因素是光照强度。&lt;/p&gt;
&lt;p&gt;若要判断提高温度对光合速率的影响，需要知道光合最适温度与当前温度的关系，题目未给出，故无法确定提高温度对曲线的影响。&lt;/p&gt;
&lt;p&gt;（4）图丁中，横轴以下代表氧气吸收速率为负，光合作用强于呼吸作用，可以积累有机物。19时以后，呼吸作用强于光合作用，开始消耗有机物。故19时的有机物积累量达到最大值&lt;/p&gt;
&lt;p&gt;（5）&lt;/p&gt;
&lt;p&gt;由题意，植株呼吸作用的氧气吸收速率恒为$12\mathrm{m}\frac{\mathrm{g}}{\mathrm{h}}$，且：&lt;/p&gt;
&lt;p&gt;$$\text{光合作用产生的O}_2\text{总量}=,,\mathrm{O}_2\text{静释放量}+\text{呼吸作用吸收的O}_2\text{总量}=,,300+12\mathrm{×}24=588\mathrm{mg}.$$&lt;/p&gt;
&lt;p&gt;由图像含义，显然阴影部分表示的$\mathrm{O}_2$释放量大于$300\mathrm{mg}$.&lt;/p&gt;
&lt;p&gt;光合速率最高时，$\mathrm{O}_2$的释放速率为$44+12=56\mathrm{m}\frac{\mathrm{g}}{\mathrm{h}}$，由于光合作用反应式为：&lt;/p&gt;
&lt;p&gt;$$6\mathrm{CO}_2+12\mathrm{H}_2\mathrm{O}\xrightarrow{\text{叶绿体} \text{光照}}\mathrm{C}&lt;em&gt;6\mathrm{H}&lt;/em&gt;{12}\mathrm{O}_6+6\mathrm{O}_2+6\mathrm{H}_2\mathrm{O}$$&lt;/p&gt;
&lt;p&gt;氧气与二氧化碳物质的量相同，又因为相对分子质量：$m(\mathrm{O}_2)=32$，$m(\mathrm{CO}_2)=44$，所以$\mathrm{CO}_2$的吸收速率为：$56\cdot \frac{44}{32}=77\mathrm{m}\frac{\mathrm{g}}{\mathrm{h}}$.&lt;/p&gt;
&lt;p&gt;（6）提高二氧化碳浓度，光合作用增强，呼吸强度不变。要想让呼吸与光合强度相等，需要降低光照强度，故B点向左移动。&lt;/p&gt;
&lt;p&gt;【考点】植物的光合作用&lt;/p&gt;
&lt;h1&gt;细胞生命历程&lt;/h1&gt;
&lt;h2&gt;3.&lt;/h2&gt;
&lt;p&gt;【答案】B&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;若该图为减数分裂，cd期可表示减II时期，无同源染色体，A错误。&lt;/p&gt;
&lt;p&gt;若该图为减数分裂，则&lt;strong&gt;基因的分离与自由组合发生在减I后期&lt;/strong&gt;，位于cd段，B正确。&lt;/p&gt;
&lt;p&gt;若该图为有丝分裂，则&lt;strong&gt;细胞板出现于有丝分裂末期&lt;/strong&gt;，对应图中ef段，C错误。&lt;/p&gt;
&lt;p&gt;若该图为有丝分裂，则ef段对应有丝分裂后期和末期，后期时有4个染色体组，末期有2个染色体组，D错误。&lt;/p&gt;
&lt;p&gt;【考点】有丝分裂、减数分裂&lt;/p&gt;
&lt;h2&gt;4.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由于减数分裂的过程，一个初级精母细胞会分裂形成四个精细胞，其中&lt;strong&gt;两两相同&lt;/strong&gt;。注意到选项均需选出三个，所以要考虑减I前期发生的交叉互换带来的影响。&lt;strong&gt;若发生交叉互换，则来自同一个次级精母细胞的两个精细胞染色体组成大致相同&lt;/strong&gt;，只是小部分有差异。因此①②⑤或③④可能来自同一个初级精母细胞，D正确。&lt;/p&gt;
&lt;p&gt;【考点】有丝分裂、减数分裂&lt;/p&gt;
&lt;h2&gt;6.&lt;/h2&gt;
&lt;p&gt;【答案】C&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;p&gt;选根尖分生区组织细胞：高等植物的分生区组织细胞有丝分裂较为旺盛。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1）解离&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使组织中的细胞相互分离开；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解离液：质量分数为15%的盐酸和体积分数为95%的酒精混合溶液。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2）漂洗&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;及时洗去解离液，防止解离过度；&lt;/p&gt;
&lt;p&gt;漂洗液：清水。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3）染色&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;p&gt;&lt;strong&gt;4）制片&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;制作玻片，利于使用显微镜观察。&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;实验基本流程为：解离-漂洗-染色-制片，A错误。&lt;/p&gt;
&lt;p&gt;解离的目的是：使组织中的细胞相互分离开，B错误。&lt;/p&gt;
&lt;p&gt;龙胆紫溶液染色可使染色体着色，利于观察，C正确。&lt;/p&gt;
&lt;p&gt;解离过程后，细胞已经死亡，显微镜只能观察细胞固定时期的分裂过程，D错误。&lt;/p&gt;
&lt;p&gt;【考点】观察有丝分裂的实验&lt;/p&gt;
&lt;h2&gt;10．&lt;/h2&gt;
&lt;p&gt;【答案】A&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;第一次分裂之前，20条染色体，40条DNA单链均被标记。&lt;/p&gt;
&lt;p&gt;第一次分裂后，每个DNA都进行了半保留复制，细胞中有20条染色体，&lt;strong&gt;每条染色体中的其中一条DNA单链有标记，另一条DNA无标记&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第二次分裂中期，DNA已完成半保留复制，但姐妹染色单体尚未分离，所以共有20条染色体。此时每个染色体由2条姐妹染色单体、4条DNA单链构成。&lt;strong&gt;4条DNA单链，有一条单链被标记&lt;/strong&gt;，故20条染色体均被标记。&lt;/p&gt;
&lt;p&gt;第二次分裂后期，姐妹染色单体分离，共40条染色体。只有20条DNA单链被标记，所以有20条染色体被标记。&lt;/p&gt;
&lt;p&gt;故A正确。&lt;/p&gt;
&lt;p&gt;【考点】有丝分裂、DNA的半保留复制&lt;/p&gt;
&lt;h2&gt;15.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;只有无秋水仙素处理组有生殖细胞产生，由图可知，生殖细胞的形成使不均等分裂，A正确。&lt;/p&gt;
&lt;p&gt;生殖细胞的形成经理细胞分裂和分化，B正确。&lt;/p&gt;
&lt;p&gt;高浓度秋水仙素可抑制纺锤体的形成，使染色体停滞在分裂中期，让细胞中染色体加倍，C正确。&lt;/p&gt;
&lt;p&gt;由题意，生殖细胞是由有丝分裂产生的，所以染色体数目不变，D错误。&lt;/p&gt;
&lt;p&gt;【考点】有丝分裂&lt;/p&gt;
&lt;h1&gt;遗传规律&lt;/h1&gt;
&lt;h2&gt;3.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【相关知识】&lt;/p&gt;
&lt;p&gt;交换值：重组型配子数占总配子数的百分率。&lt;/p&gt;
&lt;p&gt;重组型配子：在减I时期发生交叉互换而重组形成的配子。&lt;/p&gt;
&lt;p&gt;重组型：与亲代基因型均不同的后代。&lt;/p&gt;
&lt;p&gt;$$\text{交换值}=\frac{\text{重组型配子数}}{\text{总配子数}}\times 100%$$&lt;/p&gt;
&lt;p&gt;也可以测交后计算：&lt;/p&gt;
&lt;p&gt;$$\text{交换值}=\frac{\text{测交后代重组型数}}{\text{总测交后代数}}\times 100%$$&lt;/p&gt;
&lt;p&gt;每交叉互换一次，会产生两个重组型配子，因此：&lt;/p&gt;
&lt;p&gt;$$\text{发生交叉互换的初级精母细胞数量}=\frac{\text{交换值}}{2}$$&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由题意，需要考虑两组基因。由于杂合子自交后表现型&lt;strong&gt;两多两少&lt;/strong&gt;，所以发生了不完全连锁。&lt;/p&gt;
&lt;p&gt;设F1代基因型为AaBb，则A与B连锁，a与b连锁。如图，在减I前期，同源染色体的非姐妹染色单体之间发生交叉互换，&lt;strong&gt;最终产生4种配子&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/08/630b748de9e29.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以交换值：&lt;/p&gt;
&lt;p&gt;$\text{交换值}=\frac{\mathrm{Ab}+\mathrm{aB}}{\mathrm{AB}+\mathrm{Ab}+\mathrm{aB}+\mathrm{ab}}\times 100%$.&lt;/p&gt;
&lt;p&gt;测交后，&lt;strong&gt;4种配子分别有基因型&lt;/strong&gt;：AaBb，Aabb，aaBb，aabb，其中&lt;strong&gt;Aabb和aaBb是重组型，是新的基因型&lt;/strong&gt;，有不同于亲本的性状。&lt;/p&gt;
&lt;p&gt;因此，交换值可以这样计算：&lt;/p&gt;
&lt;p&gt;$\text{交换值}=\frac{\mathrm{Aabb}+\mathrm{aaBb}}{\mathrm{AaBb}+\mathrm{Aabb}+\mathrm{aaBb}+\mathrm{aabb}}\times 100%$，&lt;/p&gt;
&lt;p&gt;代入题中数据，则：&lt;/p&gt;
&lt;p&gt;$\text{交换值}=\frac{1+1}{4+1+1+4}=20%$.&lt;/p&gt;
&lt;p&gt;【考点】连锁互换&lt;/p&gt;
&lt;h2&gt;7.&lt;/h2&gt;
&lt;p&gt;【答案】&lt;/p&gt;
&lt;p&gt;（1）A，B&lt;/p&gt;
&lt;p&gt;（2）Aa×Aa&lt;/p&gt;
&lt;p&gt;（3）4&lt;/p&gt;
&lt;p&gt;① A­­+A­­+或aa&lt;/p&gt;
&lt;p&gt;② A­­+A&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;（1）A组判断出红花为显性，B组判断出蓝花为显性，矛盾。&lt;/p&gt;
&lt;p&gt;（2）Aa与Aa均为蓝花，交配后得到：&lt;/p&gt;
&lt;p&gt;$$\mathrm{Aa}\otimes \mathrm{Aa}\rightarrow \left[ \begin{matrix}     1\mathrm{AA}&amp;#x26;                    \text{蓝色}\                2\mathrm{Aa}&amp;#x26;                    \text{蓝色}\                1\mathrm{aa}&amp;#x26;                   \text{红色}\\end{matrix} \right. $$&lt;/p&gt;
&lt;p&gt;其余组合方式均不可行。&lt;/p&gt;
&lt;p&gt;（3）枚举可知，红花植株的基因型有4种可能，分别为：$\mathrm{A}^+\mathrm{A}^+,\mathrm{ A}^+\mathrm{A},\mathrm{ A}^+\mathrm{a},\mathrm{ aa}$.&lt;/p&gt;
&lt;p&gt;（3）①与所有红花杂交的结果：&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;基因型&lt;/strong&gt;                                    | &lt;strong&gt;F1&lt;/strong&gt;                               | &lt;strong&gt;表现型&lt;/strong&gt; |
| --------------------------------------------- | ------------------------------------ | ---------- |
| $\mathrm{AA}\times  \mathrm{A}^+\mathrm{A}^+$ | $\mathrm{A}^+\mathrm{A}$             | 红色       |
| $\mathrm{AA}\times  \mathrm{A}^+\mathrm{A}$   | $\mathrm{A}^+\mathrm{A},\mathrm{AA}$ | 红色、蓝色 |
| $\mathrm{AA}\times  \mathrm{A}^+\mathrm{a}$   | $\mathrm{A}^+\mathrm{A},\mathrm{Aa}$ | 红色、蓝色 |
| $\mathrm{AA}\times \mathrm{aa}$               | $\mathrm{Aa}$                        | 蓝色       |&lt;/p&gt;
&lt;p&gt;因此可以判断出的基因型是$\mathrm{A}^+\mathrm{A}^+$和$\mathrm{aa}$.&lt;/p&gt;
&lt;p&gt;（3）②与所有红花杂交的结果：&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;基因型&lt;/strong&gt;                                    | &lt;strong&gt;F1&lt;/strong&gt;                               | &lt;strong&gt;表现型&lt;/strong&gt; |
| --------------------------------------------- | ------------------------------------ | ---------- |
| $\mathrm{aa}\times  \mathrm{A}^+\mathrm{A}^+$ | $\mathrm{A}^+\mathrm{a}$             | 红色       |
| $\mathrm{aa}\times  \mathrm{A}^+\mathrm{A}$   | $\mathrm{A}^+\mathrm{a},\mathrm{Aa}$ | 红色、蓝色 |
| $\mathrm{aa}\times  \mathrm{A}^+\mathrm{a}$   | $\mathrm{A}^+\mathrm{a},\mathrm{aa}$ | 红色       |
| $\mathrm{aa}\times  \mathrm{aa}$              | $\mathrm{aa}$                        | 红色       |&lt;/p&gt;
&lt;p&gt;因此可以判断出的基因型是$\mathrm{A}^+\mathrm{A}$.&lt;/p&gt;
&lt;h2&gt;11.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;共三对等位基因，纯显性有6个基因，共270-150=120g，所以每个显性基因对重量的增加效应是20g.190g的果实有2个显性，4个隐性基因，计算概率：&lt;/p&gt;
&lt;p&gt;AaBbCc自交后，枚举可得六种基因型满足题意：&lt;/p&gt;
&lt;p&gt;$$\mathrm{AaBbCc}\otimes \mathrm{AaBbCc}\rightarrow \left[ \begin{matrix}    \mathrm{AAbbcc}&amp;#x26;                       \frac{1}{4}\times \frac{1}{4}\times \frac{1}{4}=\frac{1}{64}\               \mathrm{aaBBcc}&amp;#x26;                       \frac{1}{4}\times \frac{1}{4}\times \frac{1}{4}=\frac{1}{64}\               \mathrm{aabbCC}&amp;#x26;                       \frac{1}{4}\times \frac{1}{4}\times \frac{1}{4}=\frac{1}{64}\               \mathrm{AaBbcc}&amp;#x26;                       \frac{1}{2}\times \frac{1}{2}\times \frac{1}{4}=\frac{4}{64}\               \mathrm{AabbCc}&amp;#x26;                       \frac{1}{2}\times \frac{1}{4}\times \frac{1}{2}=\frac{4}{64}\               \mathrm{aaBbCc}&amp;#x26;                       \frac{1}{4}\times \frac{1}{2}\times \frac{1}{2}=\frac{4}{64}\\end{matrix} \right. $$&lt;/p&gt;
&lt;p&gt;相加得到概率为15/64，D选项正确。&lt;/p&gt;
&lt;h2&gt;14.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;单瓣紫罗兰自交后，性状分离，产生重瓣紫罗兰，可知单瓣对重瓣是显性性状，A错误。&lt;/p&gt;
&lt;p&gt;单瓣是显性性状，可知P的基因型为Bb，自交过程为：&lt;/p&gt;
&lt;p&gt;$$\begin{matrix}           &amp;#x26;                                {\color{gray} \mathrm{B}}&amp;#x26;                                {\color{gray} \mathrm{b}}\             {\color{gray} \mathrm{B}}&amp;#x26;                                   \mathrm{BB}&amp;#x26;                                   \mathrm{Bb}\             {\color{gray} \mathrm{b}}&amp;#x26;                                   \mathrm{Bb}&amp;#x26;                                \mathrm{bb}\\end{matrix}$$&lt;/p&gt;
&lt;p&gt;若单瓣基因纯合致死，则F1代基因型Bb:bb=2:1，与50%矛盾，B错误。&lt;/p&gt;
&lt;p&gt;若缺少B基因的配子致死，则bb基因型致死，无重瓣紫罗兰，与题意矛盾，C错误。&lt;/p&gt;
&lt;p&gt;若含B基因的雄配子不育，结合上表可知，F1中Bb:bb=1:1，符合题意，D正确。&lt;/p&gt;
&lt;h2&gt;16.&lt;/h2&gt;
&lt;p&gt;【答案】B&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;球形果的11号染色体有3条，产生的配子既可以有1条染色体，也可能有2条染色体。&lt;/p&gt;
&lt;p&gt;在杂交1中，若母本产生的配子有2条11号染色体，则子代为球形果。由结果可知，&lt;strong&gt;由球形母本产生的25%的配子有2条11号染色体&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在杂交2中，子代形状均正常，所以&lt;strong&gt;球形父本产生的包含2条11号染色体的配子均没有育性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;结合以上，球形父本只提供含有1条11号染色体的配子，球形母本有25%的配子有2条11号染色体，故球形果自花授粉的子代，球形比例约为25%，B正确。&lt;/p&gt;
&lt;h1&gt;遗传的物质基础&lt;/h1&gt;
&lt;h2&gt;1.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;四种碱基，A与T配对，C与G配对。由表格数据可知，最多4种脱氧核苷酸，&lt;strong&gt;形成2对A-T，2对C-G&lt;/strong&gt;，共4个脱氧核苷酸对，A错误。&lt;/p&gt;
&lt;p&gt;由DNA的结构可知，每个脱氧核糖与两分子磷酸相接，构成长链，B错误。&lt;/p&gt;
&lt;p&gt;先不考虑碱基对内部的顺序（A-T/T-A），用1代表A/T，2代表C/G，按DNA的含氮碱基数量分类统计：&lt;/p&gt;
&lt;p&gt;1个：序列可为1,2，共2种，不同DNA数量为：$2\times 2^1=4$；&lt;/p&gt;
&lt;p&gt;2个：序列可为11,12,21,22，共4种，不同DNA数量为：$4\times 2^2=16$；&lt;/p&gt;
&lt;p&gt;3个：序列可为112,121,211,122,212,221，共6种，不同DNA数量为：$6\times 2^3=48$；&lt;/p&gt;
&lt;p&gt;4个：序列可为1122,1212,1221,2112,2121,2211，共6种，不同DNA数量为：$6\times 2^4=96$.&lt;/p&gt;
&lt;p&gt;相加结果为：$6\times 2^4=96$，C正确。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A-T之间形成2个氢键，C-G之间形成3个氢键&lt;/strong&gt;，因此共有$2\times 2+3\times 2=10$，D错误。&lt;/p&gt;
&lt;h2&gt;3.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;HIV的增殖过程需要逆转录酶参与，但该病毒没有逆转录过程，无需逆转录酶，A错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④过程代表RNA的复制，需要RNA复制酶&lt;/strong&gt;，所以③过程发生在④过程之前，B错误。&lt;/p&gt;
&lt;p&gt;③⑥过程是翻译，④⑤过程是复制，都需要碱基配对，配对方式相同，C正确。&lt;/p&gt;
&lt;p&gt;④过程合成-RNA，⑤过程合成+RNA，两次复制消耗的尿嘧啶核糖核苷酸数目不同，D错误。&lt;/p&gt;
&lt;p&gt;【考点】基因的表达过程&lt;/p&gt;
&lt;h2&gt;7.&lt;/h2&gt;
&lt;p&gt;【答案】B&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;生物细胞内，DNA聚合酶催化5’向3’延伸。由图可知，上面的模板链合成出的子链方向是从左到右，所以c是所在链的3’端，故a是5’端，A正确。&lt;/p&gt;
&lt;p&gt;如图，c对应的链在合成过程中，是分段形成的然后相接的，B错误。&lt;/p&gt;
&lt;p&gt;DNA分子的复制过程可以有多个起点，所以C正确。&lt;/p&gt;
&lt;p&gt;酶1用于解旋碱基对的氢键，酶2用于DNA聚合，作用于磷酸二酯键，D正确。&lt;/p&gt;
&lt;p&gt;【考点】DNA的复制过程&lt;/p&gt;
&lt;h2&gt;8.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由题意，BrdU可替代胸腺嘧啶脱氧核苷酸掺入DNA子链中，因此可以与A配对，A正确。&lt;/p&gt;
&lt;p&gt;有丝分裂中期时，DNA复制已经完成，每个DNA有1条单链中含有BrdU，&lt;strong&gt;每条染色体的每条染色单体都只有1条DNA单链含有BrdU&lt;/strong&gt;，所以每个染色单体均着色深，B正确。&lt;/p&gt;
&lt;p&gt;减I中期，DNA又完成了一次复制，此时每条染色体的情况：其中一条染色单体中，2条DNA单链均含有BrdU；另一条染色单体中，1条单链含有BrdU，另一条单链不含。&lt;strong&gt;所以一个单体为深色，另一个单体为浅色。&lt;strong&gt;减I前期&lt;/strong&gt;可能发生交叉互换&lt;/strong&gt;，所以一条深色染色单体中可能会出现浅色部分，C正确。&lt;/p&gt;
&lt;p&gt;减II中期，相比于减I中期，染色体并无变化，因此&lt;strong&gt;一个单体为深色，另一个单体为浅色&lt;/strong&gt;，即使考虑交叉互换，也不可能出现每条染色体中染色单体均着色浅的情况，D错误。&lt;/p&gt;
&lt;p&gt;【考点】DNA的复制、有丝分裂、减数分裂&lt;/p&gt;
&lt;h2&gt;9.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;复制前，有2条$^{14}\mathrm{N}-\mathrm{DNA}$，在培育24h后，这2条在总单链中的占比是1/8，所以共有16条单链，8条染色体，DNA复制了3次。每24h复制3次，所以细胞周期为8h，A错误。&lt;/p&gt;
&lt;p&gt;条带的数目和位置与DNA的复制方式无关，B错误。&lt;/p&gt;
&lt;p&gt;解开DNA双螺旋的实质是破坏含氮碱基对之间的氢键，C错误。&lt;/p&gt;
&lt;p&gt;子代DNA有两种：两条链均为$^{15}\mathrm{N}$或一条链为$^{14}\mathrm{N}$，另一条链为$^{15}\mathrm{N}$，其重量不同。进行密度梯度离心，也能得到两条条带，D正确。&lt;/p&gt;
&lt;p&gt;【考点】DNA的复制过程&lt;/p&gt;
&lt;h1&gt;生物变异&lt;/h1&gt;
&lt;h2&gt;2.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由图可知，甲品系的n染色体有片段缺失，其构成的雄配子致死。所以甲产生的雄配子有NR,Nr，甲产生的雌配子有NR,Nr,nR,nr，交配过程：&lt;/p&gt;
&lt;p&gt;$\begin{matrix} &amp;#x26;    {\color{gray} \mathrm{NR}}&amp;#x26;   {\color{gray} \mathrm{Nr}}&amp;#x26;   {\color{gray} \mathrm{nR}}&amp;#x26;   {\color{gray} \mathrm{nr}}\  {\color{gray} \mathrm{NR}}&amp;#x26;   \mathrm{NNRR}&amp;#x26;   \mathrm{NNRr}&amp;#x26;   \mathrm{NnRR}&amp;#x26;   \mathrm{NnRr}\ {\color{gray} \mathrm{Nr}}&amp;#x26;   \mathrm{NNRr}&amp;#x26;   \mathrm{NNrr}&amp;#x26;   \mathrm{NnRr}&amp;#x26;   \mathrm{Nnrr}\\end{matrix}$&lt;/p&gt;
&lt;p&gt;共2种表型，6种基因型，A错误。&lt;/p&gt;
&lt;p&gt;甲产生的雄配子有NR,Nr，乙产生的雌配子是nr，所以F1只有2种基因型，B错误。&lt;/p&gt;
&lt;p&gt;丙的父本是乙，仅能产生配子r.要使后代为Rr，需要丙产生基因型为R的配子。丙的基因型为RRr，产生的配子基因型及其占比为：$\mathrm{R}:\mathrm{Rr}:\mathrm{RR}:\mathrm{r}=2:2:1:1$，有1/3的概率产生R，故C正确。&lt;/p&gt;
&lt;p&gt;产生丙的原因是RR之间分离失败，是减II异常所致，D错误。&lt;/p&gt;
&lt;p&gt;【考点】三体的配子种类&lt;/p&gt;
&lt;h2&gt;3.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;种群的基因库是基因型的总和，并非基因，A错误。&lt;/p&gt;
&lt;p&gt;进化的实质是基因频率的定向改变，并非基因型频率，B错误。&lt;/p&gt;
&lt;p&gt;B的基因频率为60%，b的基因频率为40%，由此计算基因型频率：&lt;/p&gt;
&lt;p&gt;$$\left{ \begin{array}{l} f_{\mathrm{BB}}=60%\times 60%=36%\        f_{\mathrm{Bb}}=2\times 60%\times 40%=48%\  f_{\mathrm{bb}}=40%\times 40%=16%\\end{array} \right. $$&lt;/p&gt;
&lt;p&gt;所以$\frac{f_{Bb}}{f_{BB}+f_{Bb}}=\frac{48}{48+36}=\frac{4}{7}=57.1%$，雌雄数量相等，所以杂合雌性概率为$\frac{57.1%}{2}=28.6%$，C正确。&lt;/p&gt;
&lt;p&gt;若该等位基因只位于X染色体上，因为雌雄比例为1:1，则$f_{\mathrm{X}^{\mathrm{b}}\mathrm{X}^{\mathrm{b}}}=\frac{1}{2}\times 40%\times 40%=8%$，同理可得$f_{\mathrm{X}^{\mathrm{b}}\mathrm{Y}}=20%$，D错误。&lt;/p&gt;
&lt;p&gt;【考点】基因频率与基因型频率&lt;/p&gt;
&lt;h2&gt;4．&lt;/h2&gt;
&lt;p&gt;【答案】B&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;II3患有甲病，I3和I4均不患甲病，可知甲为一种隐性遗传病。由题意，其中一种病的致病基因位于X染色体上。不妨设是甲，则由于II3患病，I3必患病，矛盾，故甲病是常染色体隐性遗传病，乙病是伴X染色体病。若乙是隐性遗传，则I4为$\mathrm{ X}^{\mathrm{b}}\mathrm{X}^{\mathrm{b}}$，II2必为$\mathrm{ X}^{\mathrm{b}}\mathrm{Y}$，与II2的表现型矛盾，所以乙是显性遗传。综上，甲病是常染色体隐性遗传病，乙病是伴X染色体显性遗传病。乙与血友病的伴X隐性遗传不同，故A错误。&lt;/p&gt;
&lt;p&gt;已知遗传方式后，容易确定每个人的基因型，则可以判断BCD的正确性&lt;/p&gt;
&lt;p&gt;B正确，CD错误。&lt;/p&gt;
&lt;p&gt;【考点】人类遗传病&lt;/p&gt;
&lt;h2&gt;10.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;有一半卵细胞直接发育为雄峰，占后代的$\frac{1}{2}$，另外一半卵细胞进行受精，也占后代的$\frac{1}{2}$。&lt;/p&gt;
&lt;p&gt;雌蜂组成为$\mathrm{X}_1\mathrm{X}_2:\mathrm{X}_2\mathrm{X}_3:\mathrm{X}_1\mathrm{X}_3=1:1:2$，所以配子及其占比为$\mathrm{X}_1:\mathrm{X}_2:\mathrm{X}_3=3:2:3$.&lt;/p&gt;
&lt;p&gt;雄峰组成为$\mathrm{X}_1\mathrm{X}_1:\mathrm{X}_3\mathrm{X}_3=1:3$，所以配子及其占比为$\mathrm{X}_1:\mathrm{X}_3=1:3$，与雌配子杂交：&lt;/p&gt;
&lt;p&gt;$$\begin{matrix}           &amp;#x26;                         {\color{gray} 3\mathrm{X}_1}&amp;#x26;                  {\color{gray} 2\mathrm{X}_2}&amp;#x26;                  {\color{gray} 3\mathrm{X}_3}\     {\color{gray} 1\mathrm{X}_1}&amp;#x26;                                3\mathrm{X}_1\mathrm{X}_1&amp;#x26;                                2\mathrm{X}_1\mathrm{X}_2&amp;#x26;                                3\mathrm{X}_1\mathrm{X}_3\            {\color{gray} 3\mathrm{X}_3}&amp;#x26;                                9\mathrm{X}_1\mathrm{X}_3&amp;#x26;                                6\mathrm{X}_2\mathrm{X}_3&amp;#x26;                            9\mathrm{X}_3\mathrm{X}_3\\end{matrix}$$&lt;/p&gt;
&lt;p&gt;由题意，纯合子发育为雄峰，所以雄峰在杂交产生后代中占比为：&lt;/p&gt;
&lt;p&gt;$$f_M=\frac{3\mathrm{X}_1\mathrm{X}_1+9\mathrm{X}_3\mathrm{X}_3}{3\mathrm{X}_1\mathrm{X}_1+2\mathrm{X}_1\mathrm{X}_2+3\mathrm{X}_1\mathrm{X}_3+9\mathrm{X}_1\mathrm{X}_3+6\mathrm{X}_2\mathrm{X}_3+9\mathrm{X}_3\mathrm{X}_3}=\frac{3}{8}$$&lt;/p&gt;
&lt;p&gt;又因为杂交后代在子代中占$\frac{1}{2}$，所以这部分雌蜂在所有子代中的占比为$\frac{1}{2}\times \frac{3}{8}=\frac{3}{16}$.&lt;/p&gt;
&lt;p&gt;综上，雄峰在子代中的占比为$\frac{1}{2}+\frac{3}{16}=\frac{11}{16}$，则雌蜂占比为$\frac{5}{16}$，可知雌：雄=5:11，C正确。&lt;/p&gt;
&lt;p&gt;【考点】分离定律&lt;/p&gt;
&lt;h2&gt;12.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;分析可知，该病为常染色体显性遗传病，6人基因型如下：&lt;/p&gt;
&lt;p&gt;$$\begin{matrix}           1&amp;#x26;                                   \mathrm{Aa}&amp;#x26;                                      &amp;#x26;                                      2&amp;#x26;                                   \mathrm{aa}\                   3&amp;#x26;                                   \mathrm{aa}&amp;#x26;                                      &amp;#x26;                                      4&amp;#x26;                                   \mathrm{Aa}\                   5&amp;#x26;                                   \mathrm{aa}&amp;#x26;                                      &amp;#x26;                                      6&amp;#x26;                                \mathrm{Aa}\\end{matrix}$$&lt;/p&gt;
&lt;p&gt;因此A错误。&lt;/p&gt;
&lt;p&gt;4号的致病基因来自父方，不可能来自母方，B错误。&lt;/p&gt;
&lt;p&gt;3号和4号婚配，4号不育，没有孩子，C错误。&lt;/p&gt;
&lt;p&gt;6与正常女性结婚，基因型为Aa的概率为1/2，是女性的概率为1/2，故孩子患病的概率为1/4，D正确。&lt;/p&gt;
&lt;p&gt;【考点】人类遗传病&lt;/p&gt;
&lt;h1&gt;植物激素调节&lt;/h1&gt;
&lt;h2&gt;2.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;由题意，该实验的自变量是拟南芥种子的类型和不同浓度的ABA，因变量是种子发芽率，A正确。&lt;/p&gt;
&lt;p&gt;由结果可知，对于野生型和突变性而言，ABA都会抑制种子发芽，且改变ABA浓度均会显著影响种子发芽率，B正确。&lt;/p&gt;
&lt;p&gt;如图，C错误。&lt;/p&gt;
&lt;p&gt;CRY1是感受光的受体，在无光环境下无法达成实验目的，D正确。&lt;/p&gt;
&lt;h2&gt;3.&lt;/h2&gt;
&lt;p&gt;【答案】C&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;根据①的结果，将突变体1的根部替换为野生型，则顶端优势恢复。由此可知，突变体1的上部正常，根部不能正常合成独脚金内酯。若施加SL，则突变体1无需根部合成SL，应表现出顶端优势，A正确。&lt;/p&gt;
&lt;p&gt;对比①②，突变体1的上部正常，所以突变体2的上部有信息传递缺陷，B正确。&lt;/p&gt;
&lt;p&gt;若地上部位也能产生SL，则突变体1的上部应有顶端优势，C错误。&lt;/p&gt;
&lt;p&gt;DL可能通过影响生长素的运输作用减少分枝的发生，这与顶端优势出现的原理相同，D正确。&lt;/p&gt;
&lt;h2&gt;6.&lt;/h2&gt;
&lt;p&gt;【答案】B&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;呼吸作用分解有机物，分解营养成分，而低温可以抑制呼吸作用，A正确。&lt;/p&gt;
&lt;p&gt;在低温状态下，有氧呼吸三个阶段所需要的酶活性均降低，所以抑制了三个阶段，B错误。&lt;/p&gt;
&lt;p&gt;低温可以降低酶的活性，C正确。&lt;/p&gt;
&lt;p&gt;乙烯可催熟，及时清除可延长果蔬保鲜时间，D正确。&lt;/p&gt;
&lt;h2&gt;7.&lt;/h2&gt;
&lt;p&gt;【答案】A&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;如图所示，IAA只与ABPI对接蛋白结合，之后停留在细胞外，不曾进入细胞，A错误。&lt;/p&gt;
&lt;p&gt;题目描述中提到：生长素与受体结合后，经过一系列的信号转换，提高细胞壁的可塑性，使细胞生长成为可能，B正确。&lt;/p&gt;
&lt;p&gt;由图可知，跨膜运输氢离子时，有ATP提供能量，需要质子泵，是主动运输，C正确。&lt;/p&gt;
&lt;p&gt;本示意图只能解释IAA促进细胞生长，不能解释抑制作用，无法解释两重性的原因，D正确。&lt;/p&gt;
&lt;h2&gt;10.&lt;/h2&gt;
&lt;p&gt;【答案】D&lt;/p&gt;
&lt;p&gt;【解析】&lt;/p&gt;
&lt;p&gt;油菜素内酯是植物生长调节剂，不是植物激素，A错误。&lt;/p&gt;
&lt;p&gt;如图，BrZ促进基因CPD的表达，B错误。&lt;/p&gt;
&lt;p&gt;BL是CPD基因表达的直接产物，并无体现，C错误。&lt;/p&gt;
&lt;p&gt;油菜素内酯与生长素作用类似，存在协同关系，D正确。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P7474】「C.E.L.U-02」学术精神</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7474</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7474</guid><description>算法题解</description><pubDate>Tue, 02 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;对于每个点 $i$ 随机与 $[1, n]$ 中的一点连无向边，若连向自己，则保留该边并再次连边，一直重复至连到别的点上为止，求边数与连通块个数期望。&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;h2&gt;期望边数&lt;/h2&gt;
&lt;p&gt;首先，注意到每个点的期望次数一定相同，所以只算一个点即可。&lt;/p&gt;
&lt;p&gt;之后利用几何分布相关定理，成功选到非自身的点的概率为 $\operatorname{p}=\frac{n-1}{n}$，期望 $\operatorname{E}=\frac{1}{p}=\frac{n}{n-1}$.&lt;/p&gt;
&lt;p&gt;下面给出证明。&lt;/p&gt;
&lt;p&gt;由期望的定义可知，设 $p=\frac{n-1}{n}$，期望的计算式为：
$$
E=1\times (1-p)^0\cdot p+2\times(1-p)^1\cdot p+3\times(1-p)^2\cdot p+\cdots+k\times (1-p)^{k-1}\cdot p
$$
设 $q=1-p$，则：
$$
E=p\cdot (1\cdot q^0+2\cdot q^1+3\cdot q^2+\cdots + k\cdot q^{k-1})
$$
设 $S=\frac{E}{p}$，错位相减求和：
$$
\begin{array}{rl}
S&amp;#x26;=1\cdot q^0+2\cdot q^1+3\cdot q^2+\cdots + k\cdot q^{k-1}\
q\cdot S&amp;#x26;=\textcolor{white}{0\cdot q^0}+1\cdot q^1+2\cdot q^2+\cdots+(k-1)\cdot q^{k-1}+k\cdot q^k\
(1-q)S &amp;#x26;= q^0+q^1+q^2+q^3+\cdots +q^{k-1}-k\cdot q^k\
(1-q)S &amp;#x26;= \dfrac{1-q^k}{1-q}-k\cdot q^k
\end{array}
$$
当 $k\rightarrow +\infty$ 时，$q^k\rightarrow 0$，所以：
$$
S=\frac{1}{(1-q)^2}
$$
期望为：
$$
E=p\cdot S=\frac{p}{(1-q)^2}=\frac{1}{p}
$$
共 $n$ 个点，所以题目所求答案为：
$$
\operatorname{Ans}=n\cdot E=\frac{n}{\frac{n-1}{n}}=\frac{n^2}{n-1}
$$&lt;/p&gt;
&lt;h2&gt;期望连通块数&lt;/h2&gt;
&lt;p&gt;如果把每个点向其他点连的边看为有向边，则每个点的出度均为 $1$，所以每个连通块都是基环树。&lt;/p&gt;
&lt;p&gt;因此：环的个数 $=$ 基环树个数 $=$ 连通块个数，下面求环的期望数量 $E$.
$$
E=\frac{\text{所有图中环的总数}}{\text{图的总数}}=\frac{A}{B}
$$
每个点都可以向外连边，图的总数为：
$$
B=(n-1)^n
$$
环的总数：先枚举环的点数 $i$，再确定哪些点在环上，其余点可以随便连边，环上的点再圆排列。
$$
A=\sum_{i=2}^n \binom{n}{i}\cdot (n-1)^{n-i}\cdot (i-1)!
$$
化简：
$$
\begin{aligned}
E &amp;#x26;= \frac{A}{B}\
&amp;#x26;= \sum_{i=2}^n \dfrac{\binom{n}{i}\cdot (n-1)^{n-i}\cdot (i-1)!}{(n-1)^n}\
&amp;#x26;= \sum_{i=2}^n \dfrac{n!\cdot (i-1)!}{i!\cdot (n-i)!\cdot (n-1)^i}\
&amp;#x26;= \sum_{i=2}^n \dfrac{n\cdot (n-1)\cdot (n-2)\cdots(n-i+1)}{i\cdot (n-1)^i}
\end{aligned}
$$
不用预处理逆元，可以 $\mathcal O(n)$ 计算。&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define int long long
using namespace std;
const int mod = 998244353;
int n, ans = 0;
inline int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		x = x * x % mod;
		k &gt;&gt;= 1;
	}
	return ret;
}
inline int inv(int x) { return power(x, mod - 2); }
signed main() {
	cin &gt;&gt; n;
	printf(&quot;%lld\n&quot;, n * n % mod * inv(n - 1) % mod);
	int tmp = n, Pow = n - 1;
	for(int i = 2; i &amp;#x3C;= n; i++) {
		tmp = tmp * (n - i + 1) % mod;
		Pow = Pow * (n - 1) % mod;
		ans = (ans + tmp * inv(i) % mod * inv(Pow) % mod) % mod;
	}
	printf(&quot;%lld\n&quot;, ans);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【Codeforces–1630B】Range and Partition</title><link>https://www.tonyyin0418.com/blog/oi-notes/cf1630b</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/cf1630b</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个长度为 $n$ 的数组 $a$。&lt;/p&gt;
&lt;p&gt;你需要确定一个范围 $[x,y]$，并将 $a$ 数组分成 $k$ 段，使得对于每一段，在范围 $[x,y]$ 以内的不同元素个数&lt;strong&gt;大于&lt;/strong&gt;在范围 $[x,y]$ 以外的不同元素个数。&lt;/p&gt;
&lt;p&gt;此处的 $x, y$ 都是权值，不是下标。&lt;/p&gt;
&lt;p&gt;请求出任意一组使得 $(y-x)$ 最小的 $x,y$，并输出划分的方案。&lt;/p&gt;
&lt;p&gt;数据范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$t$ 组数据，$1\leqslant t\leqslant 3\times 10^4$。&lt;/li&gt;
&lt;li&gt;$1\leqslant k\leqslant n\leqslant 2\times 10^5$，$\sum n\leqslant 2\times 10^5$。&lt;/li&gt;
&lt;li&gt;$1\leqslant a_i\leqslant n$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;对于这道题，&lt;strong&gt;最根本的问题&lt;/strong&gt;是最小化的 $(y-x)$，而&lt;strong&gt;具体的形式&lt;/strong&gt;是如何划分整个序列。&lt;/p&gt;
&lt;p&gt;我们可以发现，如果同时考虑这两个问题，事情会变得非常复杂，难以下手。&lt;/p&gt;
&lt;p&gt;所以我们不妨暂且扔下具体的形式，去考察最根本的问题。&lt;/p&gt;
&lt;p&gt;**性质：**划分为 $k$ 段时，在 $[x, y]$ 内的数总体上至少要比在此区间以外的数多 $k$ 个。&lt;/p&gt;
&lt;p&gt;**证明：**每个段内至少多一个，一共多至少 $k$ 个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;做法：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先将整个序列从小到大排序，然后用一个大小为 $n-\lfloor\frac{n-k}{2}\rfloor$ 的滑动窗口去检测。&lt;/p&gt;
&lt;p&gt;窗口两端的数就是 $[x, y]$，取 $(y-x)$ 的最小值即可确定 $[x, y]$。&lt;/p&gt;
&lt;p&gt;推理一下可以发现，让每一段都多一个，可以使滑动窗口尽可能小，这样 $(y-x)$ 也会相应更小，同时这样的一组 $[x, y]$ 一定有可行的划分方案。&lt;/p&gt;
&lt;p&gt;最后构造方案时也是每一段多一个就划开即可。&lt;/p&gt;</content:encoded></item><item><title>【Codeforces–743C】Vladik and fractions</title><link>https://www.tonyyin0418.com/blog/oi-notes/cf743c</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/cf743c</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定正整数 $n$，请找出一个合法的三元组 $(x, y, z)$，满足：
$$
\frac{1}{x}+\frac{1}{y}+\frac{1}{z}=\frac{2}{n}
$$
要求：$x, y, z\in \Bbb{Z}^+$ 且互不相同。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^4$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;$$
\frac{1}{x}+\frac{1}{y}+\frac{1}{z}=\frac{1}{n}+\frac{1}{n}
$$&lt;/p&gt;
&lt;p&gt;那就直接 $z=n$，则有：
$$
\frac{1}{x}+\frac{1}{y}=\frac{1}{n}
$$
考虑裂项公式：
$$
\frac{1}{n}-\frac{1}{n+1}=\frac{1}{n(n+1)}
$$
因此：
$$
\frac{1}{x} + \frac{1}{y}=\frac{1}{n+1}+\frac{1}{n(n+1)}
$$
于是得到：
$$
\left{
\begin{array}{lr}
x=n+1\
y=n(n+1)\
z=n
\end{array}
\right.
$$
当 $n=1$ 时，$x=y=n+1$，无解。&lt;/p&gt;
&lt;p&gt;否则，按上述方案构造即可。&lt;/p&gt;</content:encoded></item><item><title>构造专题分享 课件</title><link>https://www.tonyyin0418.com/blog/oi-notes/slides-20220619</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/slides-20220619</guid><description>2022-06-19 构造专题分享 课件</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You can also view or download the PDF from the following link:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/slides/slides-20220619.pdf&quot;&gt;Download PDF&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>【Codeforces–1270E】Divide Points</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1270e</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1270e</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给你 $n$ 个&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;/p&gt;
&lt;p&gt;保证存在合法方案。&lt;/p&gt;
&lt;p&gt;$2\leq n\leq 10^3$，$|x_i|, |y_i|\leq 10^6$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;将点分为黑色和白色两组。&lt;/p&gt;
&lt;p&gt;考虑对格子黑白染色，&lt;strong&gt;坐标和为偶数的染为黑色&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;一、若两种颜色的点都存在&lt;/h4&gt;
&lt;p&gt;黄色边的边长平方一定是奇数，蓝色边的边长平方一定是偶数，满足题目条件。&lt;/p&gt;
&lt;p&gt;直接按黑白颜色分组即可。&lt;/p&gt;
&lt;h4&gt;二、若只存在一种颜色的点&lt;/h4&gt;
&lt;p&gt;不妨设&lt;strong&gt;只存在黑点&lt;/strong&gt;，因为只有白点，就可以全部下移一格，转化为黑点。&lt;/p&gt;
&lt;p&gt;如果所有点的两维坐标都是偶数，则可以直接 $\div 2$，但我们只保证了 $2\mid (x+y)$.&lt;/p&gt;
&lt;p&gt;想要转化到让 $(x+y)$ 既有奇数又有偶数，过程中不改变点之间的位置关系。&lt;/p&gt;
&lt;p&gt;注意到 $2\mid (x+y)\Rightarrow 2\mid (x-y)$.&lt;/p&gt;
&lt;p&gt;联想到坐标旋转公式：
$$
\left{
\begin{aligned}
x^\prime=x\cos \theta-y\sin \theta\
y^\prime=x\sin \theta+y\cos \theta
\end{aligned}
\right.
$$
若旋转 $45\degree$，则有：
$$
\left{
\begin{aligned}
x^\prime=\frac{\sqrt{2}}{2}x-\frac{\sqrt{2}}{2}y=\frac{\sqrt{2}}{2}(x-y)\
y^\prime=\frac{\sqrt{2}}{2}x+\frac{\sqrt{2}}{2}y=\frac{\sqrt{2}}{2}(x+y)
\end{aligned}
\right.
$$
旋转和缩放都可以保证点间位置关系不变。我们只想看到整数，于是让：
$$
\left{
\begin{aligned}
x^\prime=\frac{x-y}{2}\
y^\prime=\frac{x+y}{2}
\end{aligned}
\right.
$$
这样进行若干次，最终一定会使黑白点均存在。&lt;/p&gt;
&lt;p&gt;分为了黑白点就可以由之前的方法解决，否则继续递归。&lt;/p&gt;
&lt;p&gt;递归的次数为 $\mathcal O(\log A)$ 次，其中 $A$ 为坐标值域大小。&lt;/p&gt;</content:encoded></item><item><title>【牛客练习赛100】小红的构造题</title><link>https://www.tonyyin0418.com/blog/oi-solution/nowcoder_100</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/nowcoder_100</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;https://ac.nowcoder.com/acm/contest/11251/D&lt;/p&gt;
&lt;p&gt;小红想让你构造一个长度不超过 $200000$ 的字符串，其中包含 $k$ 个 $\texttt {red}$ 子序列。你能帮帮她吗？&lt;/p&gt;
&lt;p&gt;子序列的定义：在原串中必须按顺序，可以不连续。例如，$\texttt {reddd}$ 有3个：$\underline{\texttt{red}}\texttt{dd}$，$\underline{\texttt{re}}\texttt{de}\underline{\texttt{d}}$，$\underline{\texttt{r}}\texttt{ed}\underline{\texttt{ed}}$.&lt;/p&gt;
&lt;p&gt;若无法构造，输出 $-1$，多解输出任意。&lt;/p&gt;
&lt;p&gt;数据范围：$0\leq k\leq 10^{14}$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;考虑字符串：
$$
\texttt{rerererererere}\dots
$$
在第一个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $x$ 个子序列；&lt;/p&gt;
&lt;p&gt;在第二个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $3x$ 个子序列；&lt;/p&gt;
&lt;p&gt;以此类推，&lt;/p&gt;
&lt;p&gt;在第 $k$ 个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $\frac{k\cdot (k+1)}{2}x$ 个子序列。&lt;/p&gt;
&lt;p&gt;于是把字符串长度缩小到 $n^{\frac{1}{3}}$ 级别，可以证明这样的长度一定小于 $200000$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
int tong[101010];
int main(){
    long long n,i,j,k,p;
    cin&gt;&gt;n;
    if(n==0)return cout&amp;#x3C;&amp;#x3C;&quot;d&quot;,0;
    for(i=1;i*i*i/6&amp;#x3C;=n;i++);
    i--;
    for(j=i;j&gt;0;j--){
        tong[j]+=n/(j*(j+1)/2);
        n%=j*(j+1)/2;
    }
    for(j=1;j&amp;#x3C;=i;j++){
        cout&amp;#x3C;&amp;#x3C;&quot;re&quot;;
        while(tong[j]--)cout&amp;#x3C;&amp;#x3C;&quot;d&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3599】Koishi Loves Construction</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3599</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3599</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;Task1：试判断能否构造并构造一个长度为 $n$ 的 $1 \dots n$ 的&lt;strong&gt;排列&lt;/strong&gt;，满足其 $n$ 个&lt;strong&gt;前缀和&lt;/strong&gt;在&lt;strong&gt;模 $n$ 的意义下&lt;/strong&gt;互不相同。若存在，请给出一种构造方案。&lt;/p&gt;
&lt;p&gt;Task2：试判断能否构造并构造一个长度为 $n$ 的 $1 \dots n$ 的&lt;strong&gt;排列&lt;/strong&gt;，满足其 $n$ 个&lt;strong&gt;前缀积&lt;/strong&gt;在&lt;strong&gt;模 $n$ 的意义下&lt;/strong&gt;互不相同。若存在，请给出一种构造方案。&lt;/p&gt;
&lt;p&gt;测试点类型 $1$：$10$ 分，满足 $X = 1$，$1 \leq n \leq 10$。&lt;br&gt;
测试点类型 $2$：$40$ 分，满足 $X = 1$，$1 \leq n \leq {10}^5$。&lt;br&gt;
测试点类型 $3$：$10$ 分，满足 $X = 2$，$1 \leq n \leq 10$。&lt;br&gt;
测试点类型 $4$：$40$ 分，满足 $X = 2$，$1 \leq n \leq {10}^5$。&lt;/p&gt;
&lt;h4&gt;样例输入 #1&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;1 1
8
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 #1&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 8 7 6 5 4 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输入 #2&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 1
11
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 #2&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 1 2 3 5 10 6 7 4 9 8 11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;设序列为 $a_i$，分别讨论两个子任务。&lt;/p&gt;
&lt;h3&gt;Task1&lt;/h3&gt;
&lt;p&gt;设 $s_i=(\sum\limits_{j=1}^i a_i)\bmod n$，可以有如下发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\forall l, r, s_l\equiv s_r\pmod n$ 当且仅当 $s_r-s_{l-1}\equiv 0\pmod n$. 因此不存在区间 $[l, r]$ 的区间和是 $n$ 的倍数。&lt;/li&gt;
&lt;li&gt;当 $a_i=n$ 时，$i=1$.&lt;/li&gt;
&lt;li&gt;若 $2\nmid n$ 且 $n\neq 1$，无解。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来对有解情况进行构造。&lt;/p&gt;
&lt;p&gt;发现 $n$ 必定为偶数，要想让 $s$ 构成 $\bmod n$ 意义下的完全剩余系，考虑把所有偶数在模意义下取相反数。&lt;/p&gt;
&lt;p&gt;也就是说，使：
$$
s={0, 1, -2, 3, -4, 5, \cdots, n-1}
$$
注意到，所有偶数的符号为负，再加上 $n$ 变为正数后，仍为偶数，所以 $s$ 仍然满足题意。&lt;/p&gt;
&lt;p&gt;则有 $a_i$ 的通项公式：
$$
a_i=
\left{
\begin{array}{lr}
n-i+1, &amp;#x26;2\mid i\
i-1, &amp;#x26;2\nmid i
\end{array}
\right.
$$
注意 $a_1=n\neq  0$.&lt;/p&gt;
&lt;h3&gt;Task2&lt;/h3&gt;
&lt;p&gt;设 $s_i=(\prod\limits_{j=1}^i a_i)\bmod n$，可以有如下发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\forall l, r, s_l\equiv s_r\pmod n$ 当且仅当 $\frac{s_r}{s_{l-1}}\equiv 0\pmod n$. 因此不存在区间 $[l, r]$ 的区间积是 $n$ 的倍数。&lt;/li&gt;
&lt;li&gt;当 $a_i=n$ 时，$i=n$.&lt;/li&gt;
&lt;li&gt;当 $a_i=1$ 时，$i=1$.&lt;/li&gt;
&lt;li&gt;当 $n$ 为合数时（$4$ 除外），$n\mid (n-1)!$，所以 $s_{n-1}\equiv s_n\pmod n$，无合法方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此序列开头为 $1$，末尾为 $n$，$n$ 为质数，想到 $s_i=i$ 是否可行？&lt;/p&gt;
&lt;p&gt;下面在 $s_{i-1}+1\equiv s_i\pmod n$ 的情况下构造：
$$
\begin{aligned}
s_{i-1} +1 &amp;#x26;\equiv s_{i-1} \times a_i &amp;#x26;\pmod{n}\
s_{i-1} \times (a_i-1) &amp;#x26;\equiv 1 &amp;#x26;\pmod{n}\  a_i &amp;#x26;= \operatorname{inv}(s_{i-1})+1 &amp;#x26;\pmod{n}
\end{aligned}
$$
于是可以从头开始递推完成构造。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P5595】-【XR-4】歌唱比赛</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5595</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5595</guid><description>算法题解</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;X 和 Y 各有一个权值 $v_x, v_y$.&lt;/p&gt;
&lt;p&gt;现在从最低位开始依次比较。&lt;/p&gt;
&lt;p&gt;比如 $v_x=37$，$v_y=28$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先比较最后一位，$(7,8)$，Y 暂时领先。&lt;/li&gt;
&lt;li&gt;再加上前一位，$(37,28)$，X 暂时领先。&lt;/li&gt;
&lt;li&gt;比较结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们用 &lt;code&gt;X&lt;/code&gt; 代表小 X 暂时领先，&lt;code&gt;Y&lt;/code&gt; 代表小 Y 暂时领先，那么可以写下一个字符串 &lt;code&gt;XY&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;再比如，$v_x=137$，$v_y=47$。&lt;/p&gt;
&lt;p&gt;如果我们再用 &lt;code&gt;Z&lt;/code&gt; 表示小 X 与小 Y 的点赞数暂时一样，那么写下的字符串应该为 &lt;code&gt;XYZ&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;给定最后的字符串 $s_i$，请构造一种可能的 $(v_x v_y)$。&lt;/p&gt;
&lt;p&gt;若不存在构造方案，输出 $-1$.&lt;/p&gt;
&lt;p&gt;为了方便输出，用前导零补足位数。&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$s_i \in {\texttt{X},\texttt{Y},\texttt{Z}}$，$1 \le \text{len}(s) \le 10^6$。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;XY

37
28
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;XYZ

137
047
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ZZZZZZ

000000
000000
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;XYZXYZ

-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;如果 $Z$ 不是连续的后缀，则无解。&lt;/p&gt;
&lt;p&gt;否则用 $0/1$ 构造即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e6 + 10;
string s;
int a[MAXN], b[MAXN];
int main() {
	cin &gt;&gt; s;
	int n = s.length(), flag = 0;
	for(int i = 0; i &amp;#x3C; n; i++) {
		if(flag &amp;#x26;&amp;#x26; s[i] != &apos;Z&apos;) {
			cout &amp;#x3C;&amp;#x3C; -1 &amp;#x3C;&amp;#x3C; endl;
			return 0;
		}
		if(s[i] == &apos;X&apos;) a[i] = 1, b[i] = 0;
		else if(s[i] == &apos;Y&apos;) a[i] = 0, b[i] = 1;
		else if(s[i] == &apos;Z&apos;) flag = 1;
	}
	for(int i = 0; i &amp;#x3C; n; i++) cout &amp;#x3C;&amp;#x3C; a[i]; cout &amp;#x3C;&amp;#x3C; endl;
	for(int i = 0; i &amp;#x3C; n; i++) cout &amp;#x3C;&amp;#x3C; b[i]; cout &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>构造专题分享</title><link>https://www.tonyyin0418.com/blog/oi-solution/pre-20220609</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/pre-20220609</guid><description>2022-06-19 构造专题分享 文字版</description><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;T1&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;X 和 Y 各有一个权值 $v_x, v_y$.&lt;/p&gt;
&lt;p&gt;现在从最低位开始依次比较。&lt;/p&gt;
&lt;p&gt;比如 $v_x=37$，$v_y=28$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先比较最后一位，$(7,8)$，Y 暂时领先。&lt;/li&gt;
&lt;li&gt;再加上前一位，$(37,28)$，X 暂时领先。&lt;/li&gt;
&lt;li&gt;比较结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们用 &lt;code&gt;X&lt;/code&gt; 代表小 X 暂时领先，&lt;code&gt;Y&lt;/code&gt; 代表小 Y 暂时领先，那么可以写下一个字符串 &lt;code&gt;XY&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;再比如，$v_x=137$，$v_y=47$。&lt;/p&gt;
&lt;p&gt;如果我们再用 &lt;code&gt;Z&lt;/code&gt; 表示小 X 与小 Y 的点赞数暂时一样，那么写下的字符串应该为 &lt;code&gt;XYZ&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;给定最后的字符串 $s_i$，请构造一种可能的 $(v_x v_y)$。&lt;/p&gt;
&lt;p&gt;若不存在构造方案，输出 $-1$.&lt;/p&gt;
&lt;p&gt;为了方便输出，用前导零补足位数。&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$s_i \in {\texttt{X},\texttt{Y},\texttt{Z}}$，$1 \le \text{len}(s) \le 10^6$。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;XY

37
28
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;XYZ

137
047
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ZZZZZZ

000000
000000
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;XYZXYZ

-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;如果 $Z$ 不是连续的后缀，则无解。&lt;/p&gt;
&lt;p&gt;否则用 $0/1$ 构造即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e6 + 10;
string s;
int a[MAXN], b[MAXN];
int main() {
	cin &gt;&gt; s;
	int n = s.length(), flag = 0;
	for(int i = 0; i &amp;#x3C; n; i++) {
		if(flag &amp;#x26;&amp;#x26; s[i] != &apos;Z&apos;) {
			cout &amp;#x3C;&amp;#x3C; -1 &amp;#x3C;&amp;#x3C; endl;
			return 0;
		}
		if(s[i] == &apos;X&apos;) a[i] = 1, b[i] = 0;
		else if(s[i] == &apos;Y&apos;) a[i] = 0, b[i] = 1;
		else if(s[i] == &apos;Z&apos;) flag = 1;
	}
	for(int i = 0; i &amp;#x3C; n; i++) cout &amp;#x3C;&amp;#x3C; a[i]; cout &amp;#x3C;&amp;#x3C; endl;
	for(int i = 0; i &amp;#x3C; n; i++) cout &amp;#x3C;&amp;#x3C; b[i]; cout &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;T2&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定正整数 $n$，请找出一个合法的三元组 $(x, y, z)$，满足：
$$
\frac{1}{x}+\frac{1}{y}+\frac{1}{z}=\frac{2}{n}
$$
要求：$x, y, z\in \Z^+$ 且互不相同。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^4$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;$$
\frac{1}{x}+\frac{1}{y}+\frac{1}{z}=\frac{1}{n}+\frac{1}{n}
$$&lt;/p&gt;
&lt;p&gt;那就直接 $z=n$，则有：
$$
\frac{1}{x}+\frac{1}{y}=\frac{1}{n}
$$
考虑裂项公式：
$$
\frac{1}{n}-\frac{1}{n+1}=\frac{1}{n(n+1)}
$$
因此：
$$
\frac{1}{x} + \frac{1}{y}=\frac{1}{n+1}+\frac{1}{n(n+1)}
$$
于是得到：
$$
\left{
\begin{array}{lr}
x=n+1\
y=n(n+1)\
z=n
\end{array}
\right.
$$
当 $n=1$ 时，$x=y=n+1$，无解。&lt;/p&gt;
&lt;p&gt;否则，按上述方案构造即可。&lt;/p&gt;
&lt;h1&gt;T3&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个长度为 $n$ 的数组 $a$。&lt;/p&gt;
&lt;p&gt;你需要确定一个范围 $[x,y]$，并将 $a$ 数组分成 $k$ 段，使得对于每一段，在范围 $[x,y]$ 以内的不同元素个数&lt;strong&gt;大于&lt;/strong&gt;在范围 $[x,y]$ 以外的不同元素个数。&lt;/p&gt;
&lt;p&gt;此处的 $x, y$ 都是权值，不是下标。&lt;/p&gt;
&lt;p&gt;请求出任意一组使得 $(y-x)$ 最小的 $x,y$，并输出划分的方案。&lt;/p&gt;
&lt;p&gt;数据范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$t$ 组数据，$1\leqslant t\leqslant 3\times 10^4$。&lt;/li&gt;
&lt;li&gt;$1\leqslant k\leqslant n\leqslant 2\times 10^5$，$\sum n\leqslant 2\times 10^5$。&lt;/li&gt;
&lt;li&gt;$1\leqslant a_i\leqslant n$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;对于这道题，&lt;strong&gt;最根本的问题&lt;/strong&gt;是最小化的 $(y-x)$，而&lt;strong&gt;具体的形式&lt;/strong&gt;是如何划分整个序列。&lt;/p&gt;
&lt;p&gt;我们可以发现，如果同时考虑这两个问题，事情会变得非常复杂，难以下手。&lt;/p&gt;
&lt;p&gt;所以我们不妨暂且扔下具体的形式，去考察最根本的问题。&lt;/p&gt;
&lt;p&gt;**性质：**划分为 $k$ 段时，在 $[x, y]$ 内的数总体上至少要比在此区间以外的数多 $k$ 个。&lt;/p&gt;
&lt;p&gt;**证明：**每个段内至少多一个，一共多至少 $k$ 个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;做法：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先将整个序列从小到大排序，然后用一个大小为 $n-\lfloor\frac{n-k}{2}\rfloor$ 的滑动窗口去检测。&lt;/p&gt;
&lt;p&gt;窗口两端的数就是 $[x, y]$，取 $(y-x)$ 的最小值即可确定 $[x, y]$。&lt;/p&gt;
&lt;p&gt;推理一下可以发现，让每一段都多一个，可以使滑动窗口尽可能小，这样 $(y-x)$ 也会相应更小，同时这样的一组 $[x, y]$ 一定有可行的划分方案。&lt;/p&gt;
&lt;p&gt;最后构造方案时也是每一段多一个就划开即可。&lt;/p&gt;
&lt;h1&gt;T4&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;https://ac.nowcoder.com/acm/contest/11251/D&lt;/p&gt;
&lt;p&gt;小红想让你构造一个长度不超过 $200000$ 的字符串，其中包含 $k$ 个 $\texttt {red}$ 子序列。你能帮帮她吗？&lt;/p&gt;
&lt;p&gt;子序列的定义：在原串中必须按顺序，可以不连续。例如，$\texttt {reddd}$ 有3个：$\underline{\texttt{red}}\texttt{dd}$，$\underline{\texttt{re}}\texttt{de}\underline{\texttt{d}}$，$\underline{\texttt{r}}\texttt{ed}\underline{\texttt{ed}}$.&lt;/p&gt;
&lt;p&gt;若无法构造，输出 $-1$，多解输出任意。&lt;/p&gt;
&lt;p&gt;数据范围：$0\leq k\leq 10^{14}$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;考虑字符串：
$$
\texttt{rerererererere}\dots
$$
在第一个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $x$ 个子序列；&lt;/p&gt;
&lt;p&gt;在第二个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $3x$ 个子序列；&lt;/p&gt;
&lt;p&gt;以此类推，&lt;/p&gt;
&lt;p&gt;在第 $k$ 个 $\texttt{re}$ 后面加 $x$ 个 $\texttt{d}$，可稳定增加 $\frac{k\cdot (k+1)}{2}x$ 个子序列。&lt;/p&gt;
&lt;p&gt;于是把字符串长度缩小到 $n^{\frac{1}{3}}$ 级别，可以证明这样的长度一定小于 $200000$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
int tong[101010];
int main(){
    long long n,i,j,k,p;
    cin&gt;&gt;n;
    if(n==0)return cout&amp;#x3C;&amp;#x3C;&quot;d&quot;,0;
    for(i=1;i*i*i/6&amp;#x3C;=n;i++);
    i--;
    for(j=i;j&gt;0;j--){
        tong[j]+=n/(j*(j+1)/2);
        n%=j*(j+1)/2;
    }
    for(j=1;j&amp;#x3C;=i;j++){
        cout&amp;#x3C;&amp;#x3C;&quot;re&quot;;
        while(tong[j]--)cout&amp;#x3C;&amp;#x3C;&quot;d&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;T5&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;Task1：试判断能否构造并构造一个长度为 $n$ 的 $1 \dots n$ 的&lt;strong&gt;排列&lt;/strong&gt;，满足其 $n$ 个&lt;strong&gt;前缀和&lt;/strong&gt;在&lt;strong&gt;模 $n$ 的意义下&lt;/strong&gt;互不相同。若存在，请给出一种构造方案。&lt;/p&gt;
&lt;p&gt;Task2：试判断能否构造并构造一个长度为 $n$ 的 $1 \dots n$ 的&lt;strong&gt;排列&lt;/strong&gt;，满足其 $n$ 个&lt;strong&gt;前缀积&lt;/strong&gt;在&lt;strong&gt;模 $n$ 的意义下&lt;/strong&gt;互不相同。若存在，请给出一种构造方案。&lt;/p&gt;
&lt;p&gt;测试点类型 $1$：$10$ 分，满足 $X = 1$，$1 \leq n \leq 10$。&lt;br&gt;
测试点类型 $2$：$40$ 分，满足 $X = 1$，$1 \leq n \leq {10}^5$。&lt;br&gt;
测试点类型 $3$：$10$ 分，满足 $X = 2$，$1 \leq n \leq 10$。&lt;br&gt;
测试点类型 $4$：$40$ 分，满足 $X = 2$，$1 \leq n \leq {10}^5$。&lt;/p&gt;
&lt;h4&gt;样例输入 #1&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;1 1
8
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 #1&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 8 7 6 5 4 3 2 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输入 #2&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 1
11
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;样例输出 #2&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;2 1 2 3 5 10 6 7 4 9 8 11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;设序列为 $a_i$，分别讨论两个子任务。&lt;/p&gt;
&lt;h3&gt;Task1&lt;/h3&gt;
&lt;p&gt;设 $s_i=(\sum\limits_{j=1}^i a_i)\bmod n$，可以有如下发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\forall l, r, s_l\equiv s_r\pmod n$ 当且仅当 $s_r-s_{l-1}\equiv 0\pmod n$. 因此不存在区间 $[l, r]$ 的区间和是 $n$ 的倍数。&lt;/li&gt;
&lt;li&gt;当 $a_i=n$ 时，$i=1$.&lt;/li&gt;
&lt;li&gt;若 $2\nmid n$ 且 $n\neq 1$，无解。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来对有解情况进行构造。&lt;/p&gt;
&lt;p&gt;发现 $n$ 必定为偶数，要想让 $s$ 构成 $\bmod n$ 意义下的完全剩余系，考虑把所有偶数在模意义下取相反数。&lt;/p&gt;
&lt;p&gt;也就是说，使：
$$
s={0, 1, -2, 3, -4, 5, \cdots, n-1}
$$
注意到，所有偶数的符号为负，再加上 $n$ 变为正数后，仍为偶数，所以 $s$ 仍然满足题意。&lt;/p&gt;
&lt;p&gt;则有 $a_i$ 的通项公式：
$$
a_i=
\left{
\begin{array}{lr}
n-i+1, &amp;#x26;2\mid i\
i-1, &amp;#x26;2\nmid i
\end{array}
\right.
$$
注意 $a_1=n\neq  0$.&lt;/p&gt;
&lt;h3&gt;Task2&lt;/h3&gt;
&lt;p&gt;设 $s_i=(\prod\limits_{j=1}^i a_i)\bmod n$，可以有如下发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\forall l, r, s_l\equiv s_r\pmod n$ 当且仅当 $\frac{s_r}{s_{l-1}}\equiv 0\pmod n$. 因此不存在区间 $[l, r]$ 的区间积是 $n$ 的倍数。&lt;/li&gt;
&lt;li&gt;当 $a_i=n$ 时，$i=n$.&lt;/li&gt;
&lt;li&gt;当 $a_i=1$ 时，$i=1$.&lt;/li&gt;
&lt;li&gt;当 $n$ 为合数时（$4$ 除外），$n\mid (n-1)!$，所以 $s_{n-1}\equiv s_n\pmod n$，无合法方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此序列开头为 $1$，末尾为 $n$，$n$ 为质数，想到 $s_i=i$ 是否可行？&lt;/p&gt;
&lt;p&gt;下面在 $s_{i-1}+1\equiv s_i\pmod n$ 的情况下构造：
$$
\begin{aligned}
s_{i-1} +1 &amp;#x26;\equiv s_{i-1} \times a_i &amp;#x26;\pmod{n}\
s_{i-1} \times (a_i-1) &amp;#x26;\equiv 1 &amp;#x26;\pmod{n}\  a_i &amp;#x26;= \operatorname{inv}(s_{i-1})+1 &amp;#x26;\pmod{n}
\end{aligned}
$$
于是可以从头开始递推完成构造。&lt;/p&gt;
&lt;h1&gt;T6&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给你 $n$ 个&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;/p&gt;
&lt;p&gt;保证存在合法方案。&lt;/p&gt;
&lt;p&gt;$2\leq n\leq 10^3$，$|x_i|, |y_i|\leq 10^6$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;将点分为黑色和白色两组。&lt;/p&gt;
&lt;p&gt;考虑对格子黑白染色，&lt;strong&gt;坐标和为偶数的染为黑色&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;一、若两种颜色的点都存在&lt;/h4&gt;
&lt;p&gt;黄色边的边长平方一定是奇数，蓝色边的边长平方一定是偶数，满足题目条件。&lt;/p&gt;
&lt;p&gt;直接按黑白颜色分组即可。&lt;/p&gt;
&lt;h4&gt;二、若只存在一种颜色的点&lt;/h4&gt;
&lt;p&gt;不妨设&lt;strong&gt;只存在黑点&lt;/strong&gt;，因为只有白点，就可以全部下移一格，转化为黑点。&lt;/p&gt;
&lt;p&gt;如果所有点的两维坐标都是偶数，则可以直接 $\div 2$，但我们只保证了 $2\mid (x+y)$.&lt;/p&gt;
&lt;p&gt;想要转化到让 $(x+y)$ 既有奇数又有偶数，过程中不改变点之间的位置关系。&lt;/p&gt;
&lt;p&gt;注意到 $2\mid (x+y)\Rightarrow 2\mid (x-y)$.&lt;/p&gt;
&lt;p&gt;联想到坐标旋转公式：
$$
\left{
\begin{aligned}
x^\prime=x\cos \theta-y\sin \theta\
y^\prime=x\sin \theta+y\cos \theta
\end{aligned}
\right.
$$
若旋转 $45\degree$，则有：
$$
\left{
\begin{aligned}
x^\prime=\frac{\sqrt{2}}{2}x-\frac{\sqrt{2}}{2}y=\frac{\sqrt{2}}{2}(x-y)\
y^\prime=\frac{\sqrt{2}}{2}x+\frac{\sqrt{2}}{2}y=\frac{\sqrt{2}}{2}(x+y)
\end{aligned}
\right.
$$
旋转和缩放都可以保证点间位置关系不变。我们只想看到整数，于是让：
$$
\left{
\begin{aligned}
x^\prime=\frac{x-y}{2}\
y^\prime=\frac{x+y}{2}
\end{aligned}
\right.
$$
这样进行若干次，最终一定会使黑白点均存在。&lt;/p&gt;
&lt;p&gt;分为了黑白点就可以由之前的方法解决，否则继续递归。&lt;/p&gt;
&lt;p&gt;递归的次数为 $\mathcal O(\log A)$ 次，其中 $A$ 为坐标值域大小。&lt;/p&gt;</content:encoded></item><item><title>本质不同子序列的计数问题</title><link>https://www.tonyyin0418.com/blog/oi-notes/calc-dif-sub</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/calc-dif-sub</guid><description>本质不同子序列的计数问题。包含多种 DP 方法和区间本质不同子序列计数方法。 </description><pubDate>Mon, 04 Apr 2022 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;：对于两个子序列 ${b_n}$ 和 ${c_n}$，若其元素个数不同或存在 $b_i\neq c_i$，则称它们是本质不同的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本质不同子序列&lt;/strong&gt;：要求子序列必须是&lt;strong&gt;非空&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;对于两个子序列 $a,b$，&lt;strong&gt;$a+b$ 表示将 $a$ 和 $b$ 首尾相连&lt;/strong&gt;，构成一个新的子序列。&lt;/p&gt;
&lt;h1&gt;一&lt;/h1&gt;
&lt;p&gt;给定长度为 $n$ 的序列 ${a_n}$，求在 ${a_n}$ 中有多少个本质不同的子序列。&lt;/p&gt;
&lt;p&gt;考虑设 $f_i$ 表示截至第 $i$ 个数， 有多少个本质不同子序列；设 $\operatorname{last}_{i}$ 表示数值 $i$ 的上一次出现位置，初始值为 $0$，在遍历更新 $f$ 时随时更新。&lt;/p&gt;
&lt;p&gt;则有转移方程：&lt;/p&gt;
&lt;p&gt;$$
f_i=\left{
\begin{array}{lr}
2\times f_{i-1}+1, &amp;#x26;\operatorname{last}&lt;em&gt;{a_i}=0\
2\times f&lt;/em&gt;{i-1} - f_{\operatorname{last}&lt;em&gt;{a_i} - 1}, &amp;#x26;\operatorname{last}&lt;/em&gt;{a_i}\neq0\
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若之前从未出现过 $a_i$，对任意之前出现过的子序列 $s$，$s+a_i$ 都是新的子序列；另外，$a_i$ 本身也可以作为一个新的子序列。&lt;/li&gt;
&lt;li&gt;若之前出现过 $a_i$，对于一个仅包含 $\operatorname{last}_i$ 之前的元素的子序列 $s$，$s+i$ 已经被计算过了，容斥减去。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;二&lt;/h1&gt;
&lt;p&gt;给定长度为 $n$ 的 &lt;strong&gt;01 序列&lt;/strong&gt; ${a_n}$，求在 ${a_n}$ 中有多少个本质不同的子序列。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n)$.&lt;/p&gt;
&lt;p&gt;考虑设 $f_{i,0/1}$ 表示截至第 $i$ 个数，有多少以 $0/1$ 结尾的本质不同子序列。&lt;/p&gt;
&lt;p&gt;则有转移方程：&lt;/p&gt;
&lt;p&gt;$$
f_{i,0}=\left{
\begin{array}{lr}
f_{i-1,0}+f_{i-1,1}+1, &amp;#x26;a_i=0\
f_{i-1,0}, &amp;#x26; a_i=1\
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;$$
f_{i,1}=\left{
\begin{array}{lr}
f_{i-1,1}, &amp;#x26;a_i=0\
f_{i-1,0}+f_{i-1,1}+1, &amp;#x26; a_i=1\
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;理解：&lt;/p&gt;
&lt;p&gt;若当前位置 $a_i=1$，考虑以 $1$ 结尾的子序列。对于 $f_{i-1}$ 包含的所有本质不同子序列，都直接在后面添上一个 $1$，这些子序列依然不重不漏，除此之外，以 $1$ 结尾的子串还可以为单独的一个 $1$。&lt;/p&gt;
&lt;p&gt;对于由 $0$ 结尾的子序列，$a_i$ 不能造成任何贡献。&lt;/p&gt;
&lt;p&gt;由此，此时 $f_{i,1}=f_{i-1,0}+f_{i-1,1}+1$，$f_{i,0}=f_{i-1,0}$.&lt;/p&gt;
&lt;h1&gt;三&lt;/h1&gt;
&lt;p&gt;给定长度为 $n$ 的 &lt;strong&gt;01 序列&lt;/strong&gt; ${a_n}$，$m$ 次询问子段 $a_l\sim a_r$ 中有多少个本质不同的子序列。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(m)\sim \mathcal{O}(m\log n)$.&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;二&lt;/strong&gt;的基础上，注意到转移只与 $f_{i-1}$ 有关，因此可以构造 $3\times 3$ 的矩阵，用&lt;strong&gt;矩阵快速幂&lt;/strong&gt;优化。&lt;/p&gt;
&lt;p&gt;若 $a_i=0$，&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix} f_{i-1,1}&amp;#x26; f_{i-1,0} &amp;#x26;1\end{bmatrix}\times
\begin{bmatrix}
1 &amp;#x26;1 &amp;#x26;0\
0 &amp;#x26;1 &amp;#x26;0\
0 &amp;#x26;1 &amp;#x26;1\
\end{bmatrix}=
\begin{bmatrix} f_{i,1}&amp;#x26; f_{i,0} &amp;#x26;1\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;若 $a_i=1$，&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix} f_{i-1,1}&amp;#x26; f_{i-1,0} &amp;#x26;1\end{bmatrix}\times
\begin{bmatrix}
1 &amp;#x26;0 &amp;#x26;0\
1 &amp;#x26;1 &amp;#x26;0\
1 &amp;#x26;0 &amp;#x26;1\
\end{bmatrix}=
\begin{bmatrix} f_{i,1}&amp;#x26; f_{i,0} &amp;#x26;1\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;形式化地，设 $M_0=\begin{bmatrix}1 &amp;#x26;1 &amp;#x26;0\0 &amp;#x26;1 &amp;#x26;0\0 &amp;#x26;1 &amp;#x26;1\\end{bmatrix}$，$M_1=\begin{bmatrix}1 &amp;#x26;0 &amp;#x26;0\1 &amp;#x26;1 &amp;#x26;0\1 &amp;#x26;0 &amp;#x26;1\\end{bmatrix}$，则答案矩阵为 $\prod\limits_{i=l}^rM_{a_i}$.&lt;/p&gt;
&lt;p&gt;可以预处理逆矩阵，用前缀积进一步优化。&lt;/p&gt;
&lt;h1&gt;四&lt;/h1&gt;
&lt;p&gt;给定长度为 $n$ 的 &lt;strong&gt;01 序列&lt;/strong&gt; ${a_n}$，$m$ 次询问操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把数列子段 $a_l\sim a_r$ 中的所有数取反。&lt;/li&gt;
&lt;li&gt;求数列子段 $a_l\sim a_r$ 中有多少个本质不同的子序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(m\log n)$.&lt;/p&gt;
&lt;p&gt;观察可知，区间翻转等价于交换 $3\times 3$ 矩阵的前两列。&lt;/p&gt;
&lt;p&gt;用线段树维护区间矩阵乘积，懒标记维护区间翻转操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
inline int read() {
	int ret = 0, ch = getchar();
	while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) ch = getchar();
	while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) {
		ret = ret * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	return ret;
}
const int MAXN = 1e5 + 10, mod = 1e9 + 7;
int n, m, a[MAXN], f[MAXN];
//dp[i][1] = dp[i-1][1] + dp[i-1][0] + 1, dp[i][0] = dp[i-1][0], a[i]=1
//dp[i][1] = dp[i-1][1], dp[i][0] = dp[i-1][0] + dp[i-1][1] + 1, a[i]=0
struct Matrix{
	int n, m, a[4][4];
	Matrix() { memset(a, 0, sizeof(a)); }
	friend Matrix operator * (const Matrix &amp;#x26;A, const Matrix &amp;#x26;B) {
		Matrix C;
		C.n = A.n, C.m = B.m;
		for(int i = 1; i &amp;#x3C;= C.n; i++) {
			for(int j = 1; j &amp;#x3C;= C.m; j++) {
				for(int k = 1; k &amp;#x3C;= A.m; k++) {
					C.a[i][j] = (C.a[i][j] + A.a[i][k] * B.a[k][j] % mod) % mod;
				}
			}
		}
		return C;
	}
	inline void rev() {
		for(int i = 1; i &amp;#x3C;= 3; i++) swap(a[i][1], a[i][2]);
		for(int i = 1; i &amp;#x3C;= 3; i++) swap(a[1][i], a[2][i]);
	}
} M[2];
inline Matrix unit(int n) {
	Matrix R; R.n = R.m = n;
	for(int i = 1; i &amp;#x3C;= n; i++) R.a[i][i] = 1;
	return R;
}
inline void Init_M() {
	M[0].n = M[0].m = M[1].n = M[1].m = 3;
	M[0].a[1][1] = 1, M[0].a[1][2] = 1, M[0].a[1][3] = 0;
	M[0].a[2][1] = 0, M[0].a[2][2] = 1, M[0].a[2][3] = 0;
	M[0].a[3][1] = 0, M[0].a[3][2] = 1, M[0].a[3][3] = 1;
	M[1].a[1][1] = 1, M[1].a[1][2] = 0, M[1].a[1][3] = 0;
	M[1].a[2][1] = 1, M[1].a[2][2] = 1, M[1].a[2][3] = 0;
	M[1].a[3][1] = 1, M[1].a[3][2] = 0, M[1].a[3][3] = 1;
}
Matrix t[MAXN &amp;#x3C;&amp;#x3C; 2]; int tag[MAXN &amp;#x3C;&amp;#x3C; 2], lc[MAXN &amp;#x3C;&amp;#x3C; 2], rc[MAXN &amp;#x3C;&amp;#x3C; 2];
#define ls(x) ((x) &amp;#x3C;&amp;#x3C; 1)
#define rs(x) ((x) &amp;#x3C;&amp;#x3C; 1 | 1)
inline void push_up(int id) { t[id] = t[ls(id)] * t[rs(id)]; }
inline void push_down(int id) {
	if(!tag[id]) return ;
	t[ls(id)].rev(), t[rs(id)].rev();
	tag[ls(id)] ^= 1, tag[rs(id)] ^= 1;
	tag[id] = 0;
}
void build(int id, int l, int r) {
	tag[id] = 0;
	lc[id] = l; rc[id] = r;
	if(l == r) {
		t[id] = M[a[l] == 1];
		return ;
	}
	int mid = (l + r) &gt;&gt; 1;
	build(ls(id), l, mid);
	build(rs(id), mid + 1, r);
	push_up(id);
}
void update(int id, int l, int r, int X, int Y) {
	if(X &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= Y) {
		tag[id] ^= 1;
		t[id].rev();
		return;
	}
	push_down(id);
	int mid = (l + r) &gt;&gt; 1;
	if(X &amp;#x3C;= mid) update(ls(id), l, mid, X, Y);
	if(Y &gt; mid) update(rs(id), mid + 1, r, X, Y);
	push_up(id);
}
Matrix query(int id, int l, int r, int X, int Y) {
	if(X &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= Y) return t[id];
	push_down(id);
	int mid = (l + r) &gt;&gt; 1; Matrix ret = unit(3);
	if(X &amp;#x3C;= mid) ret = ret * query(ls(id), l, mid, X, Y);
	if(Y &gt; mid) ret = ret * query(rs(id), mid + 1, r, X, Y);
	return ret;
}
void sol() {
	Init_M();
	build(1, 1, n);
	while(m--) {
		int opt = read(), l = read(), r = read(), ans;
		if(opt == 1) {
			update(1, 1, n, l, r);
		} else {
			Matrix R = query(1, 1, n, l, r);
			ans = (R.a[3][1] + R.a[3][2]) % mod;
			printf(&quot;%lld\n&quot;, ans);
		}
	}
}
signed main() {
	n = read(), m = read();
	for(int i = 1; i &amp;#x3C;= n; i++) a[i] = read();
	sol();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P7529】- 动态凸包与 CDQ 分治</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7529</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7529</guid><description>算法题解</description><pubDate>Tue, 15 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/P7529&quot;&gt;P7529 [USACO21OPEN] Permutation G&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;给定 $q$ 次询问，每次询问有两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;向凸包中加入点 $(x,y)$.&lt;/li&gt;
&lt;li&gt;给定直线 $y=kx+b$，询问是否与凸包相交。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据范围：$1\leq q\leq 10^5$.&lt;/p&gt;
&lt;h1&gt;凸包与直线的交&lt;/h1&gt;
&lt;p&gt;显然，我们首先需要维护上凸包和下凸包。&lt;/p&gt;
&lt;p&gt;对直线作两条平行线，分别切于上凸包和下凸包。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/03/16/8098fc57b4f17.svg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如图，找切点可以利用上下凸包的斜率单调性。&lt;/p&gt;
&lt;p&gt;形式化地，对于斜率为 $k$ 的直线 $Ax+By=C$，我们要在上凸包找到点 $A_i(x_{a_i},y_{a_i})$ 满足 $\operatorname{slope}(\overrightarrow {A_{i-1} A_{i} } ) &amp;#x3C; k &amp;#x3C; \operatorname{slope}(\overrightarrow {A_{i}A_{i+1}})$，在下凸包中找到点 $B_i(x_{b_i}, y_{b_i})$ 满足 $\operatorname{slope}(\overrightarrow {B_{i-1}B_{i}}) &amp;#x3C; k &amp;#x3C; \operatorname{slope}(\overrightarrow {B_{i}B_{i+1}})$.&lt;/p&gt;
&lt;p&gt;之后，将 $(x_{a_i}, y_{a_i})$ 和 $(x_{b_i}, y_{b_i})$ 代入直线方程，得到 $C_a=A\cdot x_{a_i}+B\cdot y_{a_i}$ 和 $C_b=A\cdot x_{b_i}+B\cdot y_{b_i}$.&lt;/p&gt;
&lt;p&gt;若 $C_b\leq C\leq C_a$ 或 $C_a\leq C\leq C_b$，说明直线与凸包相交，否则不交。&lt;/p&gt;
&lt;p&gt;问题只剩下如何维护凸包。&lt;/p&gt;
&lt;h1&gt;CDQ 分治优化&lt;/h1&gt;
&lt;p&gt;需要动态维护两个凸包，可以平衡树在线处理，但没必要。题目不要求在线，考虑离线下来利用 CDQ 分治优化，更加简单。&lt;/p&gt;
&lt;p&gt;对每条直线记录两个变量，$\operatorname{mx}=\max\limits_{i=1}^n(A\cdot x_i + B\cdot y_i)$，$\operatorname{mn}=\min\limits_{i=1}^n(A\cdot x_i + B\cdot y_i)$， 代表把凸包中的每个点代入方程，所得到数值的最大值和最小值。&lt;/p&gt;
&lt;p&gt;在此之后，只需判断 $\operatorname{mn} \leq C \leq \operatorname{mx}$ 是否成立即可。&lt;/p&gt;
&lt;p&gt;与CDQ分治的大致思想相同，把初始的 $n$ 个点的时间戳设为 $[1,n]$，后面的操作从 $n+1$ 开始顺序排列。&lt;/p&gt;
&lt;p&gt;设当前在处理区间 $[l,r]$，我们用 $[l,mid]$ 中的点来更新 $[mid+1, r]$ 区间中的线，这样以保证时间顺序。&lt;/p&gt;
&lt;p&gt;每次分治过程中的更新操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建上凸包，线性，保证斜率按降序排序。&lt;/li&gt;
&lt;li&gt;把直线按照斜率降序排序。&lt;/li&gt;
&lt;li&gt;CDQ 分治的常规操作，用左半部分更新右半部分中斜率在 $[\operatorname{slope}(\overrightarrow {A_{i-1}A_{i}}), \operatorname{slope}(\overrightarrow {A_{i}A_{i+1}})]$ 范围内的直线所对应的最大纵坐标。&lt;/li&gt;
&lt;li&gt;构建下凸包，直线按斜率升序排序，剩余操作与 $3$ 类似，更新纵坐标的最小值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意到，过程中利用了斜率单调性。观察发现，当 $Ax+By=C$ 的 $B&amp;#x3C;0$，也就是斜率为负时，会用上凸包更新最小值，下凸包更新最大值，会出现问题。&lt;/p&gt;
&lt;p&gt;因此，对于所有 $Ax+By=C,B&amp;#x3C;0$，进行操作 $A\leftarrow -A$，$B\leftarrow -B$，$C\leftarrow -C$ 即可。&lt;/p&gt;
&lt;h1&gt;代码实现&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
inline int read() {
	int ret = 0, ch = getchar(), f = 1;
	while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) { if(ch == &apos;-&apos;) f = -1, ch = getchar(); }
	while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) {
		ret = ret * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	return ret * f;
}
const int MAXN = 4e5 + 10, inf = 0x3f3f3f3f3f3f3f3f;
int n, Q;
struct Point{
	int opt, x, y, z;
	Point() {}
	Point(int _x, int _y) { x = _x; y = _y; }
	Point(Point a, Point b): x(b.x - a.x), y(b.y - a.y) {} //从A指向B的向量
	friend Point operator + (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x + b.x, a.y + b.y);
	}
	friend Point operator - (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x - b.x, a.y - b.y);
	}
	friend int operator * (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x * b.y - a.y * b.x;
	}
} pt[MAXN], dir[MAXN], ch[MAXN];
typedef Point Vec;
inline bool cmp_p(Point A, Point B) { return A.x &amp;#x3C; B.x || (A.x == B.x &amp;#x26;&amp;#x26; A.y &amp;#x3C; B.y); }
struct Query{
	int opt, x, y, z;
} q[MAXN];
struct Line{
	Line() {}
	Line(Point _v, int _id) { v = _v, id = _id; }
	Point v; int id;
} ln[MAXN];

inline bool cmp_l1(Line A, Line B) { return A.v * B.v &amp;#x3C; 0; }
inline bool cmp_l2(Line A, Line B) { return A.v * B.v &gt; 0; }
int mx[MAXN], mn[MAXN];
inline bool check1(Point s1, Point s2, Point p) {
	return Vec(s2, s1) * Vec(s1, p) &gt;= 0;
}
inline bool check2(Point s1, Point s2, Point p) {
	return Vec(s2, s1) * Vec(s1, p) &amp;#x3C;= 0;
}
inline void update(int id, int x, int y) {
	mx[id] = max(mx[id], x * q[id].x + y * q[id].y);
	mn[id] = min(mn[id], x * q[id].x + y * q[id].y);
}
void cdq(int l, int r) {
	if(l == r) return ;
	int mid = (l + r) &gt;&gt; 1;
	cdq(l, mid); cdq(mid + 1, r);

	int cntp = 0, cntl = 0;
	for(int i = l; i &amp;#x3C;= mid; i++) if(q[i].opt == 1) pt[++cntp] = Point(q[i].x, q[i].y);
	for(int i = mid + 1; i &amp;#x3C;= r; i++) if(q[i].opt == 2) ln[++cntl] = Line(dir[i], i);
	if(cntp == 0 || cntl == 0) return ;
	sort(pt + 1, pt + cntp + 1, cmp_p);
	//找上凸包
	int top = 0;
	ch[++top] = pt[1];
	for(int i = 2; i &amp;#x3C;= cntp; i++) {
		while(top &gt; 1 &amp;#x26;&amp;#x26; check1(ch[top], ch[top - 1], pt[i]))
			top--;
		ch[++top] = pt[i];
	}
	sort(ln + 1, ln + cntl + 1, cmp_l1);
	int j = 1;
	for(int i = 1; i &amp;#x3C;= top; i++) {
		while((i == top || ln[j].v * (ch[i + 1] - ch[i]) &amp;#x3C;= 0) &amp;#x26;&amp;#x26; j &amp;#x3C;= cntl) {
			update(ln[j].id, ch[i].x, ch[i].y);
			j++;
		}
	}
	//找下凸包
	top = 0;
	ch[++top] = pt[1];
	for(int i = 2; i &amp;#x3C;= cntp; i++) {
		while(top &gt; 1 &amp;#x26;&amp;#x26; check2(ch[top], ch[top - 1], pt[i]))
			top--;
		ch[++top] = pt[i];
	}
	sort(ln + 1, ln + cntl + 1, cmp_l2);
	j = 1;
	for(int i = 1; i &amp;#x3C;= top; i++) {
		while((i == top || ln[j].v * (ch[i + 1] - ch[i]) &gt;= 0) &amp;#x26;&amp;#x26; j &amp;#x3C;= cntl) {
			update(ln[j].id, ch[i].x, ch[i].y);
			j++;
		}
	}
}
signed main() {
	n = read(), Q = read();
	for(int i = 1; i &amp;#x3C;= n; i++) q[i].x = read(), q[i].y = read(), q[i].opt = 1;
	Q += n;
	for(int i = n + 1; i &amp;#x3C;= Q; i++) {
		q[i].opt = read();
		if(q[i].opt == 1) q[i].x = read(), q[i].y = read();
		else {
			q[i].x = read(), q[i].y = read(), q[i].z = read();
			if(q[i].y &amp;#x3C; 0) q[i].x *= -1, q[i].y *= -1, q[i].z *= -1;
			if(q[i].y == 0 &amp;#x26;&amp;#x26; q[i].x &amp;#x3C; 0) q[i].x *= -1, q[i].z *= -1;
			dir[i] = Point(q[i].y, -q[i].x);
			mx[i] = -inf, mn[i] = inf;
		}
	}
	cdq(1, Q);
	for(int i = n + 1; i &amp;#x3C;= Q; i++) if(q[i].opt == 2) {
		if(mx[i] - q[i].z &amp;#x3C; 0 || mn[i] - q[i].z &gt; 0) puts(&quot;YES&quot;);
		else puts(&quot;NO&quot;);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>数位DP</title><link>https://www.tonyyin0418.com/blog/oi-notes/number-dp</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/number-dp</guid><description>按数位进行动态规划，内容是某个高中课题中我负责的部分</description><pubDate>Tue, 08 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;解决的问题&lt;/h1&gt;
&lt;p&gt;给定区间 $[L,R]$，求区间内满足一定条件的数的个数。&lt;/p&gt;
&lt;p&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;此类问题的数据规模一般较大。$1\leq L&amp;#x3C;R\leq 10^{100000}$ 在某些情况下也是可以接受的，因为是按位DP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终目的为计数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了方便，数位DP一般使用&lt;strong&gt;记忆化搜索&lt;/strong&gt;实现。&lt;/p&gt;
&lt;h1&gt;算法原理&lt;/h1&gt;
&lt;p&gt;通常情况下，首先将求 $\operatorname{ans}&lt;em&gt;{[L,R]}$ 的问题转化为 $\operatorname{ans}&lt;/em&gt;{[1,R]}-\operatorname{ans}_{[1,l-1]}$.&lt;/p&gt;
&lt;p&gt;数位DP的核心思想，是注意到：在从 $1$ 向上数数的过程中，过程中有很多重复的部分，比如 $7000\sim7999$，$9000\sim9999$，它们的后三位都是 $_000\sim _ 999$，对这些部分加以记录，相同的部分只计算一次。&lt;/p&gt;
&lt;p&gt;可以进一步通过例题理解。&lt;/p&gt;
&lt;h1&gt;P2657 [SCOI2009] windy 数&lt;/h1&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;$\rm{windy}$ 数：&lt;strong&gt;不含前导零且相邻两个数字之差至少为 $2$ 的正整数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;给定 $a,b$，求 $[a,b]$ 中有多少个 $\rm{windy}$ 数。&lt;/p&gt;
&lt;p&gt;$1\leq a\leq b\leq 2\times 10^9$.&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;定义状态：$\rm{(pos,lst,lim,zero)}$，分别表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\rm{pos}$：当前正在尝试确定第几位，个位是第 $1$ 位，十位是第 $2$ 位，以此类推。&lt;/li&gt;
&lt;li&gt;$\rm{lst}$：本题中，需要记录上一位选的是什么数，以约束当前位置的选取。&lt;/li&gt;
&lt;li&gt;$\rm{lim}$：当前选出的前缀是否与上界相同。比如对于上界 $273967$，当前枚举出的前缀为 $2739_\ _$，则 $\operatorname{pos}=2$ 时只能选取 $[0,6]$ 的数。&lt;/li&gt;
&lt;li&gt;$\rm{zero}$：当前选出的前缀是否都为 $0$. 对于上界 $273967$，$\underline{00}9000$ 也是可以取到的，这时有 $2$ 个前导零。这与 $\operatorname{lim}$ 的作用类似，都用于标记特殊情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;
const int MAXN = 20;
int a, b, num[MAXN];
int dp[MAXN][MAXN];
int dfs(int pos, int lst, bool lim, bool zero) {
	if(pos == 0) return 1;
	if(!lim &amp;#x26;&amp;#x26; !zero &amp;#x26;&amp;#x26; dp[pos][lst] != -1) return dp[pos][lst];
	int ret = 0;
	int mx = lim ? num[pos] : 9;
	for(int i = 0; i &amp;#x3C;= mx; i++) if(abs(i - lst) &gt;= 2)
		ret += dfs(pos - 1, (zero &amp;#x26; (i == 0)) ? -2 : i, lim &amp;#x26; (i == num[pos]), zero &amp;#x26; (i == 0));
	if(!lim &amp;#x26;&amp;#x26; !zero) dp[pos][lst] = ret;
	return ret;
}
int solve(int x) {
	int p = 0;
	while(x) {
		num[++p] = x % 10;
		x /= 10;
	}
	memset(dp, -1, sizeof(dp));
	return dfs(p, -2, 1, 1);
}
int main() {
	cin &gt;&gt; a &gt;&gt; b;
	cout &amp;#x3C;&amp;#x3C; solve(b) - solve(a - 1) &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>数论基础</title><link>https://www.tonyyin0418.com/blog/oi-notes/number-theory</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/number-theory</guid><description>没写完。 带余除法、欧几里得算法、筛法、欧拉函数与欧拉定理、扩展欧几里得算法、乘法逆元、中国剩余定理、扩展中国剩余定理、卢卡斯定理、扩展卢卡斯定理</description><pubDate>Tue, 08 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;带余除法&lt;/h1&gt;
&lt;h2&gt;整除&lt;/h2&gt;
&lt;p&gt;设 $a,b$ 是整数，$a\not= 0$，如果存在 $q$ 是整数，使得 $b=aq$，那么有 $b$ 可被 $a$ 整除，记作 $a \mid b$，且称 $b$ 是 $a$ 的&lt;strong&gt;倍数&lt;/strong&gt;，$a$ 是 $b$ 的&lt;strong&gt;因数&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;若 $a \mid b$ 且 $b \mid c$，则 $\forall x,y$，有$a \mid xb+yc$.&lt;/li&gt;
&lt;li&gt;若 $a \mid b$ 且 $b \mid c$，则 $a \mid c$.&lt;/li&gt;
&lt;li&gt;设 $m\neq 0$，则 $a\mid b$，当且仅当 $ma\mid mb$.&lt;/li&gt;
&lt;li&gt;若 $a\mid b$ 且 $b\mid a$，则 $a=\pm b$.&lt;/li&gt;
&lt;li&gt;若 $a \mid b$ 且 $b\not= 0$，则 $ |a|\leq |b|$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;带余除法&lt;/h2&gt;
&lt;p&gt;设 $a,b$ 是正整数，且 $b\not= 0$，则存在唯一整数 $q, r$ 使得 $a = qb+r,0\leq r &amp;#x3C; |b|$.&lt;/p&gt;
&lt;p&gt;这个式子叫做带余除法，记余数 $r=a\bmod b$。当 $r=0$ 时，$b$ 是 $a$ 的因数。&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;p&gt;$$
(a\pm b)\bmod p=(a\bmod p\pm b\bmod p)\bmod p
$$&lt;/p&gt;
&lt;p&gt;$$
(a\times b)\bmod p=(a\bmod p\times b\bmod p)\bmod p
$$&lt;/p&gt;
&lt;h2&gt;最大公因数与最小公倍数&lt;/h2&gt;
&lt;h2&gt;最大公因数&lt;/h2&gt;
&lt;p&gt;设 $a$ 和 $b$ 是两个非零整数，如果 $d \mid a$ 且 $d \mid b$，则称 $d$ 是 $a$ 与 $b$ 的&lt;strong&gt;公因数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所有公因数中最大的被称为 $a,b$ 的&lt;strong&gt;最大公因数&lt;/strong&gt;，记为 $\gcd(a,b)$.&lt;/p&gt;
&lt;p&gt;特殊地，当 $\gcd(a,b)=1$时，称 $a,b$ &lt;strong&gt;互质&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;最小公倍数&lt;/h2&gt;
&lt;p&gt;设 $a$ 和 $b$ 是两个非零整数，如果 $a \mid d$ 且 $a \mid d$，则称 $d$ 是 $a$ 与 $b$ 的&lt;strong&gt;公倍数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所有公倍数中最小的被称为 $a,b$ 的&lt;strong&gt;最小公倍数&lt;/strong&gt;，记做 $\operatorname{lcm}(a,b)$.&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;若 $a \mid m,b \mid m$，则$\gcd(a,b) \mid m$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若 $d \mid a,d \mid b$，则 $d \mid \operatorname{lcm}(a,b)$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\operatorname{lcm}(a,b)=\dfrac{ab}{\gcd(a,b)}$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设 $m,a,b$ 是正整数，则 $\operatorname{lcm}(ma, mb)=m\times \operatorname{lcm}(a, b)$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若 $m$ 是非零整数 $a_1,a_2,a_3,\cdot\cdot\cdot, a_n$ 的公倍数，则 $\operatorname{lcm}(a_1, a_2,\cdot\cdot\cdot;, a_n) \mid m$.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;欧几里得算法&lt;/h1&gt;
&lt;p&gt;欧几里得算法，又名&lt;strong&gt;辗转相除法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有结论：
$$
\gcd(a,b)=\gcd(b,a\bmod b)
$$
设 $a=qb+r,(r&amp;#x3C;q)$，$d\mid a$ 且 $d\mid b$.&lt;/p&gt;
&lt;p&gt;由于 $a=qb+r$，故 $r=a-qb$，&lt;/p&gt;
&lt;p&gt;另一方面，由于 $d\mid a$ 且 $d\mid b$，有 $d\mid a-qb$.&lt;/p&gt;
&lt;p&gt;于是得到 $d\mid r$，所以任意 $a,b$ 的公因数都是 $b,r$ 的公因数，$r&amp;#x3C;a$，所以 $\gcd(a,b)=\gcd(b,r)$ 得证。&lt;/p&gt;
&lt;p&gt;时间复杂度为 $\mathcal{O}(\log n)$，下面是代码实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int gcd(int a, int b){
	if(b == 0) return a;
    return gcd(b, a % b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;筛法&lt;/h1&gt;
&lt;h2&gt;埃氏筛法&lt;/h2&gt;
&lt;p&gt;**问题：**给定 $n$，预处理出每个 $[1,n]$ 范围内的整数是否为质数的算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;/strong&gt;：从小到大枚举，若确定 $x$ 为质数，则将 $x, 2x, 3x, \cdots, kx$ 标记为合数。若遍历到 $x$ 时，其仍然未被标记为合数，则可确定 $x$ 为质数。&lt;/p&gt;
&lt;p&gt;还有以下优化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$j$ 的初值改为 $j=i\cdot i$，因为对于 $j=i\cdot k,(k &amp;#x3C; i)$，合数 $j$ 会被 $k$ 提前筛掉。&lt;/li&gt;
&lt;li&gt;$i$ 枚举的范围为 $[2,\sqrt{n}]$，因为对于 $i&gt;\sqrt{n}$，$i^2&gt;n$，起不到任何贡献。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 2; i &amp;#x3C;= n; i++) is_prime[i] = true;
for(int i = 2; i * i &amp;#x3C;= n; i++) {
    if(is_prime[i])
		for(int j = i * i; j &amp;#x3C;= n; j += i) is_prime[j] = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过简单优化的埃氏筛，可证明时间复杂度为 $\mathcal{O}(n\log \log n)$，相较于 $\mathcal{O}(n)$ 的线性筛，埃氏筛并不实用。&lt;/p&gt;
&lt;h2&gt;欧拉筛法&lt;/h2&gt;
&lt;p&gt;欧拉筛法，又称&lt;strong&gt;线性筛&lt;/strong&gt;，顾名思义，其时间复杂度为 $\mathcal{O}(n)$。&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 class=&quot;language-cpp&quot;&gt;int flag[N + 10], pri[N + 10], cnt = 0;
inline void Euler() {
	flag[1] = 1;
	for(int i = 2; i &amp;#x3C;= N; i++) {
		if(!flag[i]) pri[++cnt] = i;
		for(int j = 1; i * pri[j] &amp;#x3C;= N &amp;#x26;&amp;#x26; j &amp;#x3C;= cnt; j++) {
			flag[i * pri[j]] = 1;
			if(i % pri[j] == 0) break;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;线性筛莫比乌斯函数&lt;/h2&gt;
&lt;p&gt;$$
\mu(n)=
\begin{cases}
1&amp;#x26;n=1\
0&amp;#x26;n\text{ 含有平方因子}\
(-1)^k&amp;#x26;k\text{ 为 }n\text{ 的本质不同质因子个数}\ \end{cases}
$$&lt;/p&gt;
&lt;p&gt;莫比乌斯函数是积性函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int flag[N + 10], pri[N + 10], mu[N + 10], cnt;
mu[1] = 1;
for(int i = 2; i &amp;#x3C;= N; i++) {
    if(!flag[i]) pri[++cnt] = i, mu[i] = -1;
    for(int j = 1; j &amp;#x3C;= cnt &amp;#x26;&amp;#x26; i * pri[j] &amp;#x3C;= N; j++) {
        flag[i * pri[j]] = 1;
        if(i % pri[j] == 0) break;
        else mu[i * pri[j]] = -mu[i];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察线性筛的过程，当 $i\bmod \operatorname{pri}_j=0$ 时，$\operatorname{pri}_j\mid i \Rightarrow (\operatorname{pri}_j)^2\mid i\cdot \operatorname{pri_j}$，这种情况下 $\mu(i\cdot \operatorname{pri_j})=0$.&lt;/p&gt;
&lt;p&gt;否则，$\operatorname{pri_j}$ 是第一次对 $i\cdot \operatorname{pri_j}$ 产生贡献，$\mu(i\cdot \operatorname{pri_j})=-\mu(i)$.&lt;/p&gt;
&lt;h1&gt;欧拉函数与欧拉定理&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;给定 $n\in \Bbb{N}^{*}$，欧拉函数是小于等于 $n$ 的所有数中与 $n$ 互质的数的个数，记为 $\varphi(n)$.&lt;/p&gt;
&lt;p&gt;形式化地，有：
$$
\varphi(n)=\sum_{i=1}^{n} [\gcd(i,n)=1]
$$&lt;/p&gt;
&lt;h2&gt;积性函数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;积性函数&lt;/strong&gt;：满足 $f(m\cdot n)=f(m)\cdot f(n)$，$\gcd(m,n)=1$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完全积性函数&lt;/strong&gt;：满足 $f(m\cdot n)=f(m)\cdot f(n)$，不要求 $m,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;有结论：
$$
\varphi(x)=x(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_n})
$$
其中 $p_1,p_2,\cdots,p_n$ 是 $x$ 的所有质因数，例如：$\varphi(12)=12\times(1-\frac{1}{2})\times(1-\frac{1}{3})=4$.&lt;/p&gt;
&lt;p&gt;推导过程如下。&lt;/p&gt;
&lt;p&gt;首先，显然 $\varphi(1)=1$，且若 $p$ 是质数，有 $\varphi(p)=p-1$.&lt;/p&gt;
&lt;p&gt;给定质数 $p$ 和正整数 $k$，考虑计算 $\varphi(p^k)$。&lt;/p&gt;
&lt;p&gt;根据质数的性质，只有 $p$ 的倍数与 $p^k$ 不互质，因此一种计算方法是：用总数 $p^k$ 减去 $p$ 的倍数个数，即：
$$
\varphi(p^k)=p^k-p^{k-1}=p^k\times (1-\frac{1}{p})
$$
根据积性函数性质可得：
$$
\begin{aligned}
\varphi(x)&amp;#x26;=\varphi(p_1^{k_1})\times\varphi(p_2^{k_2})\times\cdots\times\varphi(p_n^{k_n})\
&amp;#x26;=p_1^{k_1}(1-\frac{1}{p_1})p_2^{k_2}(1-\frac{1}{p_2})\cdots p_n^{k_n}(1-\frac{1}{p_n})\
&amp;#x26;=x(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_n})
\end{aligned}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;inline int phi(int n) {
	int res = n;
	for(int i = 2; i * i &amp;#x3C;= n; i++) { // 分解质因数
		if(n % i == 0) { // i 是 n 的质因子
			res = res / i * (i - 1);
			while(n % i == 0) n /= i;
		}
	}
	if (n &gt; 1) //最后可能还剩一个最大的质因子
		res = res / n * (n - 1);
	return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;欧拉定理&lt;/h2&gt;
&lt;h2&gt;内容&lt;/h2&gt;
&lt;p&gt;任意选取 $a,n\in \Bbb{N}^{*}$ 满足 $\gcd(a, n)=1$，则有：
$$
a^{\varphi(n)}\equiv1\pmod n
$$&lt;/p&gt;
&lt;h2&gt;证明&lt;/h2&gt;
&lt;h2&gt;费马小定理&lt;/h2&gt;
&lt;p&gt;对任意给定的正整数 $a$ 和质数 $p$，都有：
$$
a^{p-1}\equiv 1\pmod p
$$
容易发现，这是欧拉定理的特殊情况，证明不再赘述。&lt;/p&gt;
&lt;h1&gt;扩展欧几里得算法&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;扩展欧几里得算法，用于求解&lt;strong&gt;一元一次不定方程&lt;/strong&gt; $ax+by=\gcd(a,,b)$，其中 $a,b$ 为已知系数，$x,y$ 为未知数。&lt;/p&gt;
&lt;h2&gt;裴蜀定理&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;设 $a,b$ 是不全为零的整数，则存在整数 $x,y$ 使得 $ax+by=\gcd(a, b)$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;因此，扩展欧几里得算法所求解的不定方程一定有整数解。&lt;/p&gt;
&lt;h2&gt;特解&lt;/h2&gt;
&lt;p&gt;要求出一组满足方程条件的整数解 $(x,y)$.&lt;/p&gt;
&lt;p&gt;考虑欧几里得算法的过程，$\gcd(a, b)=\gcd(b, a\bmod b)$.&lt;/p&gt;
&lt;p&gt;记 $d=\gcd(a, b)$，则要求解的方程为 $ax+by=d$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关注欧几里得算法的递归过程&lt;/strong&gt;，$(a,b)$ 递归至 $(b, a\bmod b)$ 后，有方程 $b\cdot x_1+(a\bmod b)\cdot y_1=d$.&lt;/p&gt;
&lt;p&gt;现在要找到 $(x,y)$ 与 $(x_1,y_1)$ 之间的等量关系，即：&lt;strong&gt;从递归的深层推到浅层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根据 $\bmod$ 运算的定义：
$$
b\cdot x_1+(a\bmod b)\cdot y_1=d
\Rightarrow  b\cdot x_1+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\cdot y_1=d
$$
整理得到：
$$
\begin{aligned}
d&amp;#x26;=b\cdot x_1+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\cdot y_1\
&amp;#x26;=b\cdot x_1+a\cdot y_1-(\left\lfloor\frac{a}{b}\right\rfloor\times b)\cdot y_1\
&amp;#x26;=a\cdot y_1+b\cdot(x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1)
\end{aligned}
$$
与方程 $ax+by=d$ 对应，可知一种可行的&lt;strong&gt;对应关系&lt;/strong&gt;为：
$$
\left{
\begin{array}{l}
x=y_1\
y=x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1
\end{array}
\right.
$$
按照这样的等量关系，在递归过程中计算即可。&lt;/p&gt;
&lt;p&gt;欧几里得算法的递归基为 $b=0$，此时 $\gcd(a,b)=a$，方程 $ax+by=d$ 的解为 $x=0,y=1$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int exgcd(int a, int b, int &amp;#x26;x, int &amp;#x26;y) { // 函数返回值为gcd(a,b)
	if(b == 0) {
		x = 1, y = 0;
		return a;
	}
	int r = exgcd(b, a % b, x, y); //先递归
	int t = x; //再计算x,y
	x = y;
	y = t - a / b * y;
	return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可以证明，这种方法求出 $ax+by=d$ 的特解 $(x,y)$ 必满足 $|x|\leq b$，$|y|\leq a$&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;通解&lt;/h2&gt;
&lt;p&gt;假设已求出特解为 $(x_0,y_0)$，满足 $ax_0+by_0=\gcd(a,,b)$.&lt;/p&gt;
&lt;p&gt;仍然记 $d=\gcd(a, b)$，则有
$$
a(x_0+k\frac{b}{d})+b(y_0-k\frac{a}{d})=d,k\in \Bbb{Z}
$$
显然，$k\dfrac{b}{d}$ 和 $k\dfrac{a}{d}$ 是在保证解仍为整数的前提下，可取到的&lt;strong&gt;最小整系数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以方程的通解为：
$$
\left{
\begin{array}{l}
x=x_0+k\frac{b}{d}\y=y_0-k\frac{a}{d}
\end{array}
\right.
$$
对于任意二元一次不定方程 $ax+by=c,(a,b,c\in \Bbb{Z})$，当且仅当 $d\mid c$ 时才有整数解。&lt;/p&gt;
&lt;p&gt;在求出 $ax+by=\gcd(a,b)$ 后，可以得到方程特解为 $(\frac{c}{d}x_0, \frac{c}{d}y_0)$，且有通解为：
$$
\left{
\begin{array}{l}
x=\frac{c}{d}x_0+k\frac{b}{d}\y=\frac{c}{d}y_0-k\frac{a}{d}\end{array}
\right.
$$&lt;/p&gt;
&lt;h2&gt;最小非负整数解&lt;/h2&gt;
&lt;p&gt;由通解形式，可用 &lt;code&gt;(x % (b/d) + (b/d)) % (b/d)&lt;/code&gt; 求得最小非负整数解。&lt;/p&gt;
&lt;p&gt;这是因为在 C++ 语言中，负整数 $x$ 进行运算 &lt;code&gt;x % p&lt;/code&gt; 的结果满足 $-p&amp;#x3C;s\leq 0$.&lt;/p&gt;
&lt;h2&gt;同余方程&lt;/h2&gt;
&lt;p&gt;同余方程，指形如 $ax\equiv b\pmod c$ 的方程。其中 $a,b,c$ 是已知量，$x$ 是未知量。&lt;/p&gt;
&lt;p&gt;注意到：
$$
ax\equiv b\pmod c\Leftrightarrow xa+kc=b
$$
于是转化为一般的&lt;strong&gt;一元一次不定方程&lt;/strong&gt;，可用&lt;strong&gt;扩展欧几里得算法&lt;/strong&gt;求解。&lt;/p&gt;
&lt;h1&gt;乘法逆元&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;给定两个整数 $a$ 和 $p$，假设存在 $x$ 使得 $ax\equiv 1\pmod p$，则称 $x$ 为 $a$ 关于 $p$ 的&lt;strong&gt;乘法逆元&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;乘法逆元又称数论倒数，是模意义下的除法，可记为 $x\equiv a^{-1}\pmod p$.&lt;/p&gt;
&lt;p&gt;可以证明，$a$ 关于 $p$ 的逆元存在的充要条件是 $\gcd(a,p)=1$.&lt;/p&gt;
&lt;h2&gt;求一个数的逆元&lt;/h2&gt;
&lt;p&gt;将上面式子变形，变成$ax+kp=1$，用扩展欧几里得能够求出一个$x$.&lt;/p&gt;
&lt;p&gt;若 $p$ 是质数，可利用&lt;strong&gt;费马小定理&lt;/strong&gt;求逆元。
$$
a^{p-1}\bmod p=(a^{p-2}\times a)\bmod p=(x\times a)\bmod p=1
$$
所以 $a$ 关于 $p$ 的逆元是 $a^{p-2}\bmod p$ 的值，用快速幂优化，时间复杂度为 $\mathcal{O}(\log n)$.&lt;/p&gt;
&lt;p&gt;代码实现不难。&lt;/p&gt;
&lt;h2&gt;求一组数的逆元&lt;/h2&gt;
&lt;p&gt;给定 $a_1, a_2, \cdots, a_n$ 和模数 $p$，若他们都与 $p$ 互质，则可以在 $\mathcal{O}(n+\log a_i)$ 的时间复杂度内计算出每个数的逆元。&lt;/p&gt;
&lt;p&gt;预处理前缀积 $\operatorname{s}&lt;em&gt;i=(\prod&lt;/em&gt;{i=1}^i a_i) \bmod p$，先计算出前缀积数组的逆元 $t_i$.&lt;/p&gt;
&lt;p&gt;$t_n$ 可通过扩展欧几里得算法或快速幂计算得到，对于 $t_{1\sim n-1}$，可以递推计算：&lt;/p&gt;
&lt;p&gt;在模意义下，有：
$$
\begin{aligned}
t_i&amp;#x26;\equiv \frac{1}{a_1a_2\cdots a_i}\pmod p\
t_{i+1}\equiv\frac{1}{a_1a_2\cdots a_i a_{i+1}}\pmod p
\end{aligned}
$$
所以：
$$
\begin{aligned}
t_i&amp;#x26;\equiv \frac{1}{a_1a_2\cdots a_i}\equiv \frac{a_{i+1}}{a_1a_2\cdots a_i a_{i+1}}\
&amp;#x26;\equiv t_{i+1}\cdot a_{i+1}\pmod p
\end{aligned}
$$
求出 $t_i$ 后，设 $a_i$ 的逆元为 $\operatorname{inv_i}$，则 $\operatorname{inv}&lt;em&gt;1=t_1$，其余项可以直接计算：
$$
\operatorname{inv}&lt;em&gt;i\equiv \frac{a_1a_2\cdots a&lt;/em&gt;{i-1}}{a_1a_2\cdots a&lt;/em&gt;{i-1}a_i}\equiv t_i\cdot s_{i-1}\pmod p
$$&lt;/p&gt;
&lt;h2&gt;组合数取模&lt;/h2&gt;
&lt;p&gt;按照上面的方法，代入 $a_i=i$，求出 $s_i=i!$，$t_i={s_i}^{-1}=\dfrac{1}{i!}$.&lt;/p&gt;
&lt;p&gt;根据组合数的定义：
$$
C_n^m\equiv \binom{n}{m}\equiv \frac{n!}{m!(n-m)!}\equiv s_n\cdot t_m\cdot t_{n-m}\pmod p
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int N = 1e6, mod = 1e9 + 7;
inline int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		x = x * x % mod, k &gt;&gt;= 1;
	}
	return ret;
}
int fac[N + 10], inv[N + 10];
inline void init() {
	fac[0] = 1;
	for(int i = 1; i &amp;#x3C;= N; i++) fac[i] = fac[i - 1] * i % mod;
	inv[N] = power(fac[N], mod - 2);
	for(int i = N - 1; i &gt;= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;中国剩余定理&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;有物不知其数，三三数之剩二，五五数之剩三，七七数之剩二。问物几何?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题就可以转化为一个同余方程组。设 $x$ 为物品数量，则有：
$$
\left{
\begin{align}{}
x\equiv 2\pmod 3\
x\equiv 3\pmod 5\
x\equiv 2\pmod 7
\end{align}{}
\right.
$$&lt;/p&gt;
&lt;h2&gt;内容&lt;/h2&gt;
&lt;p&gt;中国剩余定理一元同余线性方程组：
$$
\left{
\begin{aligned}&amp;#x26;x\equiv a_1\pmod {m_1}\&amp;#x26;x\equiv a_2\pmod {m_2}\&amp;#x26;\cdots\&amp;#x26;x\equiv a_n\pmod {m_n}\end{aligned}
\right.
$$
假设整数 $m_1,m_2,\cdots,m_n$ &lt;strong&gt;两两互质&lt;/strong&gt;，则方程组必有解，通解可以构造如下：&lt;/p&gt;
&lt;p&gt;设 $M=\prod m_i$，并设 $M_i=\dfrac{M}{m_i}$， $t_i\equiv M_i^{-1}\pmod {m_i}$，有通解为：&lt;/p&gt;
&lt;p&gt;$$
x=a_1t_1M_1+a_2t_2M_2+\cdots+a_nt_nM_n+kM=\sum_{i-1}^{n}a_it_iM_i+kM
$$&lt;/p&gt;
&lt;p&gt;这不难理解。在 $\bmod m_i$ 意义下，整数 $j$ 满足：
$$
a_jt_jM_j\equiv \left{
\begin{aligned}
0, &amp;#x26;j\neq i\
a_j, &amp;#x26;j=i
\end{aligned}
\right. \pmod {m_i}
$$
因为 $j\neq i$ 时，$M_j\equiv 0\pmod {m_i}$，且有 $t_iM_i\equiv 1\pmod {m_i}$.&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;程序的返回值为最小整数解。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int exgcd(int a, int b, int &amp;#x26;x, int &amp;#x26;y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	int r = exgcd(b, a % b, x, y);
	int t = x;
	x = y;
	y = t - a / b * y;
	return r;
}
int a[101], m[101];
inline int CRT(int n) {
	int M = 1;
	int ans=0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		M *=m [i];
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		int x, y;
		int Mi = M / m[i];
		exgcd(Mi, m[i], x, y);
		ans = (ans + Mi * x * a[i]) % M;
	}
	if(ans &amp;#x3C; 0) ans += M;
	return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;扩展中国剩余定理&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;同样用于求解同余方程组，与中国剩余定理的区别是&lt;strong&gt;模数不一定互质&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;内容&lt;/h2&gt;
&lt;p&gt;考虑两个同余方程：
$$
\left{
\begin{aligned}x\equiv a_1 \pmod{m_1}\
x\equiv a_2\pmod{m_2}
\end{aligned}
\right.
$$
可以写为：
$$
\left{
\begin{aligned}
x+k_1m_1=a_1&amp;#x26;&amp;#x26;(1)\
x+k_2m_2=a_2&amp;#x26;&amp;#x26;(2)
\end{aligned}
\right.
$$
$(1)-(2)$ 得到：
$$
m_1k_1-m_2k_2=a_1-a_2
$$
可见，方程形如 $ax+by=c$，所以有解条件是 $\gcd(m1,m2)\mid (a_1-a_2)$.&lt;/p&gt;
&lt;p&gt;用扩展欧几里得算法求得可行解 $(K_1, K_2)$，因此 $K_1$ 的通解为：
$$
K_1+\frac{m_2}{d}\cdot C
$$
代入 $(1)$，得到：
$$
x+K_1m_1+\frac{m_2m_1}{d}\cdot C=a_1
$$
写成同余方程的形式：
$$
x\equiv a_1-K_1m_1 \pmod{\operatorname{lcm}(m_1, m_2)}
$$
这样就把两个同余方程合并成了一个。&lt;/p&gt;
&lt;p&gt;同理，可以将 $n$ 个方程组成的同余方程组化为一个同余方程，最后用扩展欧几里得算法求解 $x$.&lt;/p&gt;
&lt;h1&gt;卢卡斯定理&lt;/h1&gt;
&lt;p&gt;对于&lt;strong&gt;质数&lt;/strong&gt; $p$，有：
$$
\binom{n}{m}\bmod p = \binom{\left\lfloor\frac{n}{p}\right\rfloor}{\left\lfloor\frac{m}{p}\right\rfloor} \cdot \binom{n\bmod p}{m\bmod p}\bmod p
$$
当 $p\leq 10^6$ 且 $n,m$ 较大时实用。&lt;/p&gt;
&lt;h1&gt;扩展卢卡斯定理&lt;/h1&gt;
&lt;p&gt;123&lt;/p&gt;</content:encoded></item><item><title>斯特林数</title><link>https://www.tonyyin0418.com/blog/oi-notes/stirling</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/stirling</guid><description>斯特林数学习笔记，zyc讲的，没学完</description><pubDate>Wed, 02 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;第二类斯特林数&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;第二类斯特林数，用 $S(n,m)$ 或 $\begin{Bmatrix}n\m\end{Bmatrix}$ 表示：将 $n$ 个有标号元素分为 $m$ 个 &lt;strong&gt;非空&lt;/strong&gt; 无标号集合的方案数。&lt;/p&gt;
&lt;h2&gt;递推式&lt;/h2&gt;
&lt;p&gt;$$
\begin{Bmatrix}n\m\end{Bmatrix}=\begin{Bmatrix}n-1\m-1\end{Bmatrix} + m\times \begin{Bmatrix}n-1\m\end{Bmatrix}
$$&lt;/p&gt;
&lt;h2&gt;通项公式&lt;/h2&gt;
&lt;p&gt;$$
\begin{Bmatrix}n\m\end{Bmatrix}=\sum_{i=0}^{m} \frac{i ^ n(-1)^{m-i}}{i!(m-i)!}
$$&lt;/p&gt;
&lt;h2&gt;行&lt;/h2&gt;
&lt;p&gt;$n$ 固定，$m$ 变。&lt;/p&gt;
&lt;p&gt;注意到通项公式很像卷积式，直接设：&lt;/p&gt;
&lt;p&gt;$$
[n^i]g(n)=\dfrac{i^n}{i!}, [n^i]h(n)=\dfrac{(-1)^{i}}{i!}
$$&lt;/p&gt;
&lt;p&gt;由于通项公式中 $i + (m-i) = m$，直接卷积得到 $f=g*h$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Poly A, B;
A.a.resize(n+1), B.a.resize(n+1);
for(int i = 0; i &amp;#x26;lt;= n; i++) A.a[i] = power(i, n) * fac_inv[i] % mod;
for(int i = 0; i &amp;#x26;lt;= n; i++) B.a[i] = (i&amp;#x26;1) ? mod - fac_inv[i] : fac_inv[i];
A = A * B;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列&lt;/h2&gt;
&lt;p&gt;TODO.&lt;/p&gt;
&lt;h1&gt;第一类斯特林数&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;第一类斯特林数，用 $s(n,m)$ 或 $\begin{bmatrix}n\m\end{bmatrix}$ 表示：将 $n$ 个有标号元素分为 $m$ 个&lt;strong&gt;非空&lt;/strong&gt;无标号圆排列的方案数。&lt;/p&gt;
&lt;h2&gt;递推式&lt;/h2&gt;
&lt;p&gt;$$
\begin{bmatrix}n\k\end{bmatrix}=\begin{bmatrix}n-1\k-1\end{bmatrix}+(n-1)\begin{bmatrix}n-1\k\end{bmatrix}
$$&lt;/p&gt;
&lt;h1&gt;行&lt;/h1&gt;
&lt;p&gt;https://www.luogu.com.cn/blog/oylyer/solution-p5408&lt;/p&gt;
&lt;p&gt;记：&lt;/p&gt;
&lt;p&gt;$$
F_n(x)=x^{\overline n}=\prod_{i=0}^n (x+i)
$$&lt;/p&gt;
&lt;p&gt;则可证：&lt;/p&gt;
&lt;p&gt;$$
x^{\overline n}=\sum_{i=1}^n \begin{bmatrix}n\i\end{bmatrix} x^i
$$&lt;/p&gt;
&lt;p&gt;由于显然有 $x^{\overline{2n}}=x^{\overline n}(x+n)^{\overline n}$，考虑分治求解。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
F_n(x+n) &amp;#x26;=\sum_{i=1}^n \begin{bmatrix}n\i\end{bmatrix} (x+n)^i\
&amp;#x26;=\sum_{i=1}^n\begin{bmatrix}n\i\end{bmatrix}\sum_{j=0}^i \binom{i}{j}x^jn^{i-j}\
&amp;#x26;=\sum_{i=1}^n\begin{bmatrix}n\i\end{bmatrix}\sum_{j=0}^i \frac{i!}{j!(i-j)!}x^jn^{i-j}\
&amp;#x26;=\sum_{j=0}^n x^j \frac{1}{j!} \sum_{i=j}^n\begin{bmatrix}n\i\end{bmatrix} i!\frac{n^{i-j}}{(i-j)!}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;设 $A_i=\begin{bmatrix}n\i\end{bmatrix} i!$，$B_i=\dfrac{n^i}{i!}$，则原式化为：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
F_n(x+n)
&amp;#x26;=\sum_{j=0}^i x^j \frac{1}{j!} \sum_{i=1}^n\begin{bmatrix}n\i\end{bmatrix} i!\frac{n^{i-j}}{(i-j)!}\
&amp;#x26;=\sum_{j=0}^i x^j \frac{1}{j!} \sum_{i=1}^n A_iB_{i-j}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;要凑卷积的形式，把 $B$ 翻转得到 $B^{\prime}_{n-i}=B_i$，则：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
F_n(x+n)
&amp;#x26;=\sum_{j=0}^i x^j \frac{1}{j!} \sum_{i=1}^n A_iB_{i-j}\
&amp;#x26;=\sum_{j=0}^i x^j \frac{1}{j!} \sum_{i=1}^n A_i{B^{\prime}}_{n+j-i}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;这样 $A*{B^{\prime}}$ 之后，得到的 $n+j$ 次项系数左移 $n$ 位，再整体 $\times \frac{1}{j!}$，就是 $F_n(x+n)$ 的 $j$ 次项系数。&lt;/p&gt;
&lt;p&gt;再把 $F_n(x)*F_n(x+n)$，即可得到 $F_{2n}(x)$。&lt;/p&gt;
&lt;p&gt;注意分治过程中，$F_{2n+1}=F_n(x)&lt;em&gt;F_n(x+n)&lt;/em&gt;(x+2n)$，遇到奇数就手动乘上去即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void solve(int x) { //求f(x)
	//x的一次上升幂f(x)=x，零次项系数为0，一次项系数为1
	if(x == 1) { F.a.resize(2); F.a[1] = 1; return; }
	//递归，通过f(n)求出f(x)
	solve(x &amp;#x26;gt;&amp;#x26;gt; 1);
	int n = (x &amp;#x26;gt;&amp;#x26;gt; 1);
	//计算出A、B
	A.a.resize(n+1), B.a.resize(n+1);
	for(int i = 0; i &amp;#x26;lt;= n; i++) A.a[i] = F.a[i] * fac[i] % mod;
	for(int i = 0, pw = 1; i &amp;#x26;lt;= n; i++, pw = pw * n % mod) 
        B.a[i] = pw * fac_inv[i] % mod;
	reverse(B.a.begin(), B.a.begin() + n + 1);
	//A与B卷积，把第n+1~2n位向左平移，之后A的每项系数除以阶乘，F再卷上A
	A = A * B;
	for(int i = 0; i &amp;#x26;lt;= n; i++) A.a[i] = A.a[i + n];
	for(int i = 0; i &amp;#x26;lt;= n; i++) A.a[i] = A.a[i] * fac_inv[i] % mod;
	A.a.resize(n+1);
	F = A * F;
	//若x是奇数，则2*n=x-1，上升幂需要再卷上一个(x+2*n)，左移，每个系数都乘上2*n
	if(x &amp;#x26; 1) {
		F.a.resize(2 * n + 2);
		for(int i = 2 * n + 1; i &amp;#x26;gt;= 1; i--) 
            F.a[i] = (F.a[i - 1] + (x - 1) * F.a[i] % mod) % mod;
		F.a[0] = F.a[0] * (x - 1) % mod;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列&lt;/h2&gt;
&lt;p&gt;TODO。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P7287】「EZEC-5」魔法</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7287</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7287</guid><description>算法题解</description><pubDate>Sun, 06 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定数列 $a_1, a_2, \cdots, a_n$，可以对其进行下列操作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;花费 $a$ 魔法值，选择 $A$ 中的一个区间 $[l,r]$，将 $a_l, a_{l+1},\cdots, a_r$ 全部 $+1$.&lt;/li&gt;
&lt;li&gt;花费 $b$ 魔法值，选择 $A$ 中的一个区间 $[l,r]$，将 $a_l, a_{l+1},\cdots, a_r$ 全部 $\times 2$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在可以做若干次操作，使得序列 $a$ 存在一个子区间，其元素之和不小于 $s$.&lt;/p&gt;
&lt;p&gt;求需要花费的最小魔法值。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^5$，$1\leq a,b\leq 10^9$，$-10^9\leq a_i\leq 10^9$，$1\leq s\leq 10^9$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;对于 $+1$ 操作，对整个序列都使用，一定不劣于只对序列的一段区间使用。&lt;/p&gt;
&lt;p&gt;对于正数，直接 $\times 2$ 一定优于 $+1$.&lt;/p&gt;
&lt;p&gt;对于负数，需要先做若干次 $+1$ 到正数，之后再 $\times 2$，这样最优。&lt;/p&gt;
&lt;p&gt;因此最终的操作序列一定先加后乘，形如：
$$
+++\cdots++\times \times \times \cdots \times \times \times
$$
由于 $1\leq s\leq 10^9$，若通过乘法操作能使区间和变大，则最多进行 $\log_2s$ 次乘法操作。&lt;/p&gt;
&lt;p&gt;$\log_2 s &amp;#x3C; 30$，所以可以枚举乘法操作的次数。&lt;/p&gt;
&lt;p&gt;对于加法操作的次数，可以二分解决。&lt;/p&gt;
&lt;p&gt;二分，每次 $\rm{check}$ 时，先线性求出执行完操作之后的新序列 $b$，之后递推地求出最大子段和。&lt;/p&gt;
&lt;p&gt;求最大子段和，只需设 $f_i$ 表示以 $i$ 结尾的最大子段和，$f_i=\max{f_{i-1}+b_i, b_i}$，$f$ 的最大值即为序列的最大子段和。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int __int128
using namespace std;
inline int read() {}
inline void out(int x) {}
const int MAXN = 1e5 + 10, inf = 0x3f3f3f3f3f3f3f3f;
int n, va, vb, s, a[MAXN], b[MAXN], ans = inf;
bool check(int add, int Pow) {
	for(int i = 1; i &amp;#x3C;= n; i++) b[i] = (a[i] + add) * Pow;
	int curf = 0, maxf = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		curf = max(curf + b[i], b[i]);
		maxf = max(maxf, curf);
	}
	return maxf &gt;= s;
}
signed main() {
	n = read(), va = read(), vb = read(), s = read();
	for(int i = 1; i &amp;#x3C;= n; i++) a[i] = read();
	//外层枚举乘法次数
	for(int x = 0, Pow = 1; x &amp;#x3C;= 50; x++, Pow &amp;#x3C;&amp;#x3C;= 1ll) {
		int l = 0, r = 1e15, mid, mmin = inf;
		//内层二分加法次数
		while(l &amp;#x3C;= r) {
			mid = (l + r) &gt;&gt; 1ll;
			if(check(mid, Pow)) r = mid - 1, mmin = mid;
			else l = mid + 1;
		}
		if(mmin != inf)
			ans = min(ans, vb * x + va * mmin);
	}
	out(ans); putchar(&apos;\n&apos;);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P7568】「MCOI-05」追杀</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7568</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7568</guid><description>算法题解</description><pubDate>Sun, 06 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;有 $m$ 位玩家 $1\sim m$，初始血量均为 $3$。玩家活着当且仅当生命值大于 $0$.&lt;/p&gt;
&lt;p&gt;有 $n$ 次按时间顺序给定的追杀操作，每次形如 $(u_k,v_k)$，追杀操作按顺序进行。&lt;/p&gt;
&lt;p&gt;执行操作 $u$ 追杀 $v$ 时，若二者都活着，那么 $v$ 的生命值减 $1$；否则不执行该操作。&lt;/p&gt;
&lt;p&gt;现在有一名特殊玩家 $0$ 号，可以选取任意 $(i,v)$，其中 $1\leq i\leq n+1$，$1\leq v\leq m$，表示时空穿越到第 $i$ 次追杀前，并追杀玩家 $v$。&lt;/p&gt;
&lt;p&gt;显然 $(i,v)$ 共有 $(n+1)\times m$ 个不同的选择方法，且不同的 $(i,v)$ 会导致最终存活玩家的集合不同。&lt;/p&gt;
&lt;p&gt;对每个 $0\leq x\leq m$，求有多少种 $(i,v)$ 的选取方法，使最终恰有 $x$ 位玩家存活，输出 $m+1$ 个数。&lt;/p&gt;
&lt;p&gt;$1\leq n\le 6\times 10^4$，$1\leq m\leq 10^3$，$1\leq u_i,v_i\leq m$.&lt;/p&gt;
&lt;p&gt;对于 $16%$ 的数据，$n,m\leq 100$.&lt;/p&gt;
&lt;p&gt;对于另外 $41%$ 的数据，$n\leq 10^3$.&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\leq n\leq 6\times 10^4$，$1\leq m\leq 10^3$，$1\leq u_i,v_i\leq m$.&lt;/p&gt;
&lt;p&gt;可以接受 $\mathcal{O}(nm)$ 的复杂度。&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;p&gt;暴力？$\mathcal{O}(n^2m)$.&lt;/p&gt;
&lt;p&gt;设 $f_{i,j}$ 表示：若在 $i$ 前穿越，追杀 $j$，最后存活的玩家数量。&lt;/p&gt;
&lt;p&gt;对于追杀操作 $(u,v)$，无论在它前面还是后面，插入一个操作 $(0,x≠u)$，对结果的影响都是相同的。&lt;/p&gt;
&lt;p&gt;因此，只有当 $j=u_i$ 时，$f_{i+1,j}≠f_{i,j}$，其他情况下都有 $f_{i,j}=f_{i+1,j}$.&lt;/p&gt;
&lt;p&gt;所以我们尝试在每个操作 $(u,v)$ 前插入追杀 $u$ 的操作，插入后 $\mathcal{O}(n)$ 模拟一遍进程，更新 $f_{i,u_i}$.&lt;/p&gt;
&lt;p&gt;这样的复杂度是 $\mathcal{O}(n^2)$ 的。&lt;/p&gt;
&lt;p&gt;注意到，若第 $i$ 次追杀操作没有生效，也有 $f_{i,j}=f_{i+1,j}$，因此只需对所有生效的操作重新跑一遍。&lt;/p&gt;
&lt;p&gt;最多有 $3m$ 次生效操作，复杂度变为 $\mathcal{O}(nm)$，可以通过。&lt;/p&gt;</content:encoded></item><item><title>【洛谷–P2183】[国家集训队]礼物</title><link>https://www.tonyyin0418.com/blog/oi-solution/p2183</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p2183</guid><description>算法题解</description><pubDate>Wed, 02 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;有 $n$ 件礼物，要送给 $m$ 个人，其中送给第 $i$ 个人 $w_i$ 件。&lt;/p&gt;
&lt;p&gt;求送礼物的方案数，对 $P$ 取模。&lt;/p&gt;
&lt;p&gt;两个方案不同，当且仅当存在某个人在这两种方案中收到的礼物数量不同。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^9$，$1\leq m\leq 5$，$\leq w_i\leq P\leq 10^9$.&lt;/p&gt;
&lt;p&gt;令 $P=\prod_{i=1}^{t}p_i^{c_i}$，则 $1\leq p_i^{c_i}\leq 10^5$.&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;p&gt;$$
\operatorname{Ans} = \binom{n}{w_1}\binom{n-w_1}{w_2}\binom{n-w1-w2}{w3}\cdots\binom{n-w1-w2-w3-w4}{w5}\bmod P
$$&lt;/p&gt;
&lt;p&gt;因为 $P$ 不一定是质数，扩展卢卡斯求解即可。&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 1e6 + 10, MAXM = 6;
int p, n, m;
int w[MAXM], sum[MAXM];
int a[MAXN], b[MAXN], cnt = 1; //余数，模数
int power(int x, int k, int p) {
    int ans = 1;
    while(k) {
        if(k &amp;#x26; 1) ans = (ans * x) % p;
        k &gt;&gt;= 1;
        x = (x * x) % p;
    }
    return ans;
}
int ExGcd(int a, int b, int &amp;#x26;x, int &amp;#x26;y) {
    if(b == 0) {
        x = 1, y = 0;
        return a;
    }
    int r = ExGcd(b, a % b, x, y);
    int t = x;
    x = y;
    y = t - a / b * y;
    return r;
}
int inv(int a, int p) {
    int x, y;
    ExGcd(a, p, x, y);
    return (x % p + p) % p;
}
int fac(int n, int p, int pk) {
    if(n == 0)return 1;
    int ans = 1;
    for(int i = 1; i &amp;#x3C; pk; i++) {
        if(i % p != 0) {
            ans = ans * i % pk;
        }
    }
    ans = power(ans, n / pk, pk);
    for(int i = 1; i &amp;#x3C;= n % pk; i++) {
        if(i % p != 0) {
            ans = ans * i % pk;
        }
    }
    return ans * fac(n / p, p, pk) % pk;
}
int C(int n, int m, int p, int pk) { //pk=p^k，求(C_n^m)%(p^k)
    int fac1 = fac(n, p, pk), fac2 = fac(m, p, pk), fac3 = fac(n - m, p, pk);
	int k = 0;
    for(int i = n; i; i /= p)
		k += i / p;
    for(int i = m; i; i /= p)
		k -= i / p;
    for(int i = n - m; i; i /= p)
		k -= i / p;
    return fac1 * inv(fac2, pk) % pk
				* inv(fac3, pk) % pk
				* power(p, k, pk) % pk;
}
int CRT() {
    int M = 1, ans = 0;
    for(int i = 1; i &amp;#x3C;= cnt; i++)
        M *= b[i];
    for(int i = 1; i &amp;#x3C;= cnt; i++) {
        int add = a[i] * (M / b[i]) % M * inv(M / b[i], b[i]) % M;
        ans = (ans + add) % M;
    }
    return ans;
}
int ExLucas(int n, int m, int p) {
    memset(a, 0, sizeof(a));
    memset(b, 0, sizeof(b));
    cnt = 0;
    for(int i = 2; i * i &amp;#x3C;= p; i++) {
        int pk = 1;
        while(p % i == 0) {
            pk *= i;
            p /= i;
        }
        if(pk &gt; 1) {
            b[++cnt] = pk;
            a[cnt] = C(n, m, i, pk);
        }
    }
    if(p &gt; 1) {
        b[++cnt] = p;
        a[cnt] = C(n, m, p, p);
    }
    return CRT();
}
signed main() {
    scanf(&quot;%lld%lld%lld&quot;, &amp;#x26;p, &amp;#x26;n, &amp;#x26;m);
    for(int i = 1; i &amp;#x3C;= m; i++)
		scanf(&quot;%lld&quot;, &amp;#x26;w[i]);
    for(int i = 1; i &amp;#x3C;= m; i++)
        sum[i] = sum[i - 1] + w[i];
    if(sum[m] &gt; n){
        cout &amp;#x3C;&amp;#x3C; &quot;Impossible&quot;;
        return 0;
    }
    int ans = 1;
    for(int i = 1; i &amp;#x3C;= m; i++) {
        ans = (ans * ExLucas(n - sum[i - 1], w[i], p)) % p;
    }
    cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces-1552H】Guess the Perimeter</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1552h</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1552h</guid><description>算法题解</description><pubDate>Fri, 28 Jan 2022 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;，且四条边均与 $x$ 或 $y$ 轴平行。&lt;/p&gt;
&lt;p&gt;你需要在最多 &lt;strong&gt;4&lt;/strong&gt; 次询问中，求出这个矩形的周长。&lt;/p&gt;
&lt;p&gt;每次询问，第一行输出 &lt;code&gt;? k&lt;/code&gt;，表示你要询问 $k$ 个点，第二行输出 $k$ 个坐标，表示询问的是这 $k$ 个点。之后，交互库会返回询问的 $k$ 个点&lt;strong&gt;在矩形内（包含边界）的点的个数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;若你已经确定了答案为 $p$，请输出 &lt;code&gt;! p&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个坐标系的范围是 $1\leq x,y \leq 200$.&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;p&gt;求周长 $\Leftrightarrow$ 求出两个边长。&lt;/p&gt;
&lt;p&gt;设矩形边上分别有 $x,y$ 个点（面积为 $(x-1)(y-1)$，周长为 $x+y-2$）&lt;/p&gt;
&lt;p&gt;先对整个棋盘都查询一遍，得到 $s=x\times y$，还剩三次查询。&lt;/p&gt;
&lt;p&gt;考虑隔行查询，对所有形如 $(ki, j)$,$1\leq i\leq \lfloor\frac{200}{d}\rfloor,1\leq j\leq 200$ 的点进行查询，设答案为 $f(k)$.&lt;/p&gt;
&lt;p&gt;哪些 $f(k)$ 能与已经查询过的 $f(1)$ 产生联系？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/02/06/70099296d74c7.svg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图，$2^k\mid x\Leftrightarrow k \times  f(2^k)=f(1)$，注意到，若 $2^k\mid x$ 且 $2^{k+1}\nmid x$，则有：
$$
|f(2^k)-2f(2^{k+1})|=y
$$
显然 $k\in{0,1,2,3,4,5,6,7}$，有八种可能，直接二分即可将询问次数降至 $3$ 次。&lt;/p&gt;
&lt;p&gt;二分：若 $2^t\nmid x$，说明 $k\in[0, t-1]$.&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P1450】[HAOI2008]硬币购物</title><link>https://www.tonyyin0418.com/blog/oi-solution/p1450</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p1450</guid><description>算法题解</description><pubDate>Fri, 28 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1450&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;共有 $4$ 种硬币。面值分别为 $c_1,c_2,c_3,c_4$。&lt;/p&gt;
&lt;p&gt;某人去商店买东西，去了 $n$ 次，对于每次购买，他带了 $d_i$ 枚第 $i$ 种硬币，想购买价值恰好为 $s$ 的东西。&lt;/p&gt;
&lt;p&gt;多次询问，每次询问形如 $(d_1,d_2,d_3,d_4,s)$，求付款方法数。&lt;/p&gt;
&lt;p&gt;$1\leq c_i,d_i,s\leq 10^5$，$1\leq n\leq 1000$.&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;p&gt;先不考虑硬币个数限制，用 $f_i$ 表示随意凑出 $i$ 元的方案数，预处理出 $f$.&lt;/p&gt;
&lt;p&gt;对每个硬币背包转移，若四种硬币面额为 $c_1,c_2,c_3,c_4$，则：
$$
f_i=\sum_{j=1}^{4}f_{i-c}
$$
再看硬币个数的限制。&lt;/p&gt;
&lt;p&gt;如果只有一种面值为 $c$ 的硬币，限制最多用 $d$ 枚，想要凑出 $s$ 元。&lt;/p&gt;
&lt;p&gt;相较于所求，$f_s$ 中多余的部分，就是取出了至少 $(d+1)$ 枚硬币。&lt;/p&gt;
&lt;p&gt;根据 $f_s$ 的定义可知，取至少 $(d+1)$ 枚硬币后凑出 $s$ 的方案数为 $f_{s-c\times(d+1)}$.&lt;/p&gt;
&lt;p&gt;可以理解为：先花掉 $(d+1)$ 枚硬币，剩下随便凑。&lt;/p&gt;
&lt;p&gt;所以只有一种硬币时，$\operatorname{Ans}=f-f_{s-c\times(d+1)}$.&lt;/p&gt;
&lt;p&gt;现在就可以开始解决本题中四种硬币的情况了，也是同样的思路，&lt;strong&gt;考虑容斥&lt;/strong&gt;。
$$
f[s]-\sum_{i=1}^{4}f[s-c_i\times (d_i+1)]
$$
直接这样减下去，同时不满足两个硬币的情况会重复，要加回来。&lt;/p&gt;
&lt;p&gt;依次类推，同时不满足三个条件的要减下去，同时不满足四个的要加回来。&lt;/p&gt;
&lt;p&gt;这个过程可以用二进制枚举方便地解决。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int N = 1e5;
int n, c[4], d[4], s, ans;
int f[N + 10]; //f[s]表示：四种面值随便取，取出s的方案数量
signed main() {
	cin &gt;&gt; c[0] &gt;&gt; c[1] &gt;&gt; c[2] &gt;&gt; c[3] &gt;&gt; n;
	f[0] = 1;
	for(int i = 0; i &amp;#x3C; 4; i++) {
		for(int j = c[i]; j &amp;#x3C;= N; j++) {
			f[j] += f[j - c[i]];
		}
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		cin &gt;&gt; d[0] &gt;&gt; d[1] &gt;&gt; d[2] &gt;&gt; d[3] &gt;&gt; s;
		ans = 0;
		for(int sta = 0; sta &amp;#x3C; (1 &amp;#x3C;&amp;#x3C; 4); sta++) {
			int cnt = 0, sum = 0;
			for(int k = 0; k &amp;#x3C; 4; k++) {
				cnt ^= (1 &amp;#x26; (sta &gt;&gt; k));
				sum += (1 &amp;#x26; (sta &gt;&gt; k)) * c[k] * (d[k] + 1);
			}
			if(s - sum &gt;= 0)
				ans += f[s - sum] * (cnt ? -1ll : 1ll);
		}
		cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3214】-[HNOI2011]卡农</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3214</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3214</guid><description>算法题解</description><pubDate>Fri, 28 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;有 $n$ 个音，$m$ 个音乐片段构成一首歌。音乐的每个片段都是从 $n$ 个音阶中挑选若干个音阶同时演奏出来。&lt;/p&gt;
&lt;p&gt;要求：任意两个片段所包含的音阶集合都不同。&lt;/p&gt;
&lt;p&gt;要求：每个音在这首歌中被奏响的次数为偶数。&lt;/p&gt;
&lt;p&gt;求满足上述条件的歌一共有多少种，答案对 $10^8+7$ 取模。&lt;/p&gt;
&lt;p&gt;两段音乐 $a$ 和 $b$ 同种当且仅当将 $a$ 的片段重新排列后可以得到 $b$。&lt;/p&gt;
&lt;p&gt;$1\le n,m \le 10^6$。&lt;/p&gt;
&lt;h1&gt;题解&lt;/h1&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;$n$ 个音，$m$ 个片段。&lt;/p&gt;
&lt;p&gt;题目忽略顺序，我们忽略要求，最后用答案 $\times \frac{1}{m!}$ 即可。&lt;/p&gt;
&lt;p&gt;设 $f_i$ 表示前 $i$ 个片段的可选方案数。&lt;/p&gt;
&lt;p&gt;利用二进制转化一下：&lt;/p&gt;
&lt;p&gt;每种选择方式编号为 $1,2,\cdots, 2^n-1$。选 $m$ 个数，异或和为 $0$，且不存在两个相同。&lt;/p&gt;
&lt;p&gt;先不考虑任意两个数不相同：&lt;/p&gt;
&lt;p&gt;前 $i$ 个数的可选方案数，不是 $C_{2^n-1}^i$，因为需要保证异或和为 $0$。&lt;/p&gt;
&lt;p&gt;注意到，若前 $i-1$ 个数已经确定，那么第 $i$ 个数则唯一固定，所以方案数为 $C_{2^n-1}^{i-1}$.&lt;/p&gt;
&lt;p&gt;但这并不完全正确，有两种不合法的情况需要考虑：&lt;/p&gt;
&lt;h2&gt;情况一&lt;/h2&gt;
&lt;p&gt;若前 $i-1$ 个数异或和为 $0$，第 $i$ 个数只能为 $0$，但可选数中不存在 $0$ 这个数。&lt;/p&gt;
&lt;p&gt;这种情况的方案数为 $f_{i-1}$.&lt;/p&gt;
&lt;h2&gt;情况二&lt;/h2&gt;
&lt;p&gt;根据组合数，我们选取的前 $i-1$ 个数一定两两不等，但第 $i$ 个数有可能和前面中的某个数相等，需要减去这种情况。&lt;/p&gt;
&lt;p&gt;不妨设第 $j$ 个数与第 $i$ 个数相等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;其他 $i-2$ 个数异或和为 $0$、且选取方式完全合法，有 $f_{i-2}$ 种选择方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$j$ 这个位置有 $(i-1)$ 种可能，每个位置下，都可以从 $1\sim 2^n-1$ 中选取，唯一的限制是不能与其他 $(i-2)$ 个数相等，有 $(i-1)[2^n-1-(i-2)]$ 种选择方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上，第二组不合法情况的方案数为 $f_{i-2}(i-1)[2^n-1-(i-2)]$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将这两种情况减去即可。&lt;/strong&gt;
$$
f_i=C_{2^n-1}^{i-1}-f_{i-1}-f_{i-2}(i-1)(2^n-1-(i-2))
$$
递推求解，
$$
\operatorname{Ans}=\frac{f_m}{m!}\bmod 100000007
$$&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P8000】[WFOI - 01] 循环节</title><link>https://www.tonyyin0418.com/blog/oi-solution/p8000</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p8000</guid><description>细节较多的计算几何题。用到了凸包、旋转卡壳、和亿些分类讨论。</description><pubDate>Tue, 25 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;给出稍微详细一点的解释，并附代码实现。&lt;/p&gt;
&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P8000&quot;&gt;题面地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定点集 $a$，已知 $a$ 是由任意三点不共线，任选四点均非梯形或平行四边形的点集 $b$ 延向量 $\vec x$ 连续移 $z$ 次得到的。每次移动后，原来的点集仍存在。&lt;/p&gt;
&lt;p&gt;求 $(b,\vec x,z)$，用坐标形式表示 $\vec x$.&lt;/p&gt;
&lt;p&gt;若有多解，求出 $z$ 最大的；若还有多解，输出任意一组即可。&lt;/p&gt;
&lt;p&gt;保证无论 $b$ 延 $\vec x$ 移多少次，都不会有重点。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^5$，点的坐标范围 $\in(-10^9, 10^9)$，均为整数，保证有解。&lt;/p&gt;
&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;代码中使用了向量类，可参考题解最后一部分的代码。&lt;/p&gt;
&lt;p&gt;为方便取模等操作，代码中下标均从 $0$ 开始记录。&lt;/p&gt;
&lt;h1&gt;求 $b$&lt;/h1&gt;
&lt;p&gt;假设我们已经求出了 $\vec x$ 和 $z$，现在想求出原点集 $b$。&lt;/p&gt;
&lt;p&gt;可以发现，点 $p\in b \Leftrightarrow p-\vec x\notin a$，因为如果一个点是经过平移操作后才得到的，$p-\vec x$ 一定存在于点集 $a$ 中。&lt;/p&gt;
&lt;p&gt;坐标范围较大，需要用到 &lt;code&gt;std::map&lt;/code&gt; 并重载运算符。&lt;/p&gt;
&lt;p&gt;注意特殊处理 $z=0$ 的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;vector&amp;#x3C;int&gt; b;
void calc_b() { //利用 map 求 b 集合
	map&amp;#x3C;Point, bool&gt; mp;
	if(z == 0) x.x = inf, x.y = inf;
	for(int i = 0; i &amp;#x3C; n; i++) mp[p[i]] = 1;
	for(int i = 0; i &amp;#x3C; n; i++) {
		if(mp[p_in[i] - x] == 0) {
			b.push_back(i);
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;求 $\vec x$ 和 $z$&lt;/h1&gt;
&lt;p&gt;求 $\vec x$ 和 $z$ 的过程较为复杂。&lt;/p&gt;
&lt;p&gt;一种暴力的方法是，$\mathcal{O}(n^2)$ 枚举所有点对 $(u,v)$，检验 $\vec x=&amp;#x3C;u,v&gt;$ 是否合法。&lt;/p&gt;
&lt;p&gt;注意到，由于点都是平移出来的，所以 $\vec x$ 会被枚举到很多遍，但我们只需要一遍。&lt;/p&gt;
&lt;p&gt;可以先随便举例观察一些性质。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/01/25/e5631d4956448.svg&quot; alt=&quot;P8000-1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;旋转卡壳&lt;/h2&gt;
&lt;p&gt;如上图，$\vec x=(2, 1)$，$b={A,B,C,D}$，$a={A,A^{\prime},A^{\prime\prime},B,B^{\prime},B^{\prime\prime},C,C^{\prime},C^{\prime\prime},D,D^{\prime},D^{\prime\prime}}$.&lt;/p&gt;
&lt;p&gt;可以发现，$\vec x$ 一定会出现在凸包的边界上。并且，存在凸包上的一条边，完整地包含：某个起点以及由其平移 $z$ 次得到的所有点。&lt;/p&gt;
&lt;p&gt;又注意到题目限制：&lt;strong&gt;没有任意四个起点构成梯形或平行四边形&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以先求凸包，然后在凸包上跑一遍旋转卡壳。若当前的两条平行线经过 $\geq 4$ 个点，则这些点一定不都是起点。我们取出平行线通过的点，利用其求出 $\vec x$ 和 $z$，方法见下一部分。&lt;/p&gt;
&lt;p&gt;形式化地，假设凸包上的点按逆时针顺序编号，当前枚举到的线段是 $(i,i+1)$，通过旋转卡壳，我们找到最大的范围 $[j,k]$，其满足：编号在 $[j,k]$ 区间内的点共线，它们所在的直线与线段 $(i,i+1)$ 平行。若 $k-j&gt;0$，则我们利用 $[j,k]$ 这些点求出 $\vec x$ 和 $z$.&lt;/p&gt;
&lt;h2&gt;处理直线上的问题&lt;/h2&gt;
&lt;p&gt;进一步把问题抽象，我们得到有 $n$ 个共线的点 $p_0,p_1,\cdots,p_{n-1}$，要利用它们求出 $\vec x$ 和 $z$.&lt;/p&gt;
&lt;p&gt;分类讨论，共三种情况，分别 $\rm{check}$ 一下就行了。&lt;/p&gt;
&lt;h3&gt;情况一&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/01/25/73ff54ddc0d90.svg&quot; alt=&quot;P8000-2&quot;&gt;&lt;/p&gt;
&lt;p&gt;只包含一个起点，$\vec x$ 为 $p_0$ 和 $p_1$ 之间的向量。&lt;/p&gt;
&lt;p&gt;因为凸包编号有序，所以起点为 $p_0$，$x=(p_{1x}-p_{0x}, p_{1y}-p_{0y})$，$z=n-1$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;curx = p[1] - p[0], ok = 1;
for(int t = 1; t &amp;#x3C; n; t++) {
    if(p[t] != p[0] + t * curx) {
        ok = 0, tmp = t;
        break;
    }
}
if(ok) {
    if(n - 1 &gt; z) z = n - 1, x = curx;
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;情况二&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/01/25/0a4be64757ad6.svg&quot; alt=&quot;P8000-3&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\vec x$ 依旧为 $p_0$ 和 $p_1$ 之间的向量&lt;/p&gt;
&lt;p&gt;包含两个起点，且两个起点延伸出来的线段，互相不交叉。&lt;/p&gt;
&lt;p&gt;在情况一中，我们用 $\rm{tmp}$ 存储了第一个不满足情况一的点的编号。&lt;/p&gt;
&lt;p&gt;由此，所以起点为 $p_0$ 和 $p_{\operatorname{tmp}}$，$x=(p_{1x}-p_{0x}, p_{1y}-p_{0y})$，在 $\rm{check}$ 的同时计算 $z$ 就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;curx = p[1] - p[0];
ok = 1, cnt1 = 0, cnt2 = 0;
for(int t = 1; t &amp;#x3C; n; t++) if(t != tmp) {
    if(p[t] == p[0] + (cnt1 + 1) * curx) cnt1++;
    else if(p[t] == p[tmp] + (cnt2 + 1) * curx) cnt2++;
    else { ok = 0; break; }
}
if(ok) {
    z = cnt1, x = curx;
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;情况三&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/01/25/89aa7add26f5c.svg&quot; alt=&quot;P8000-4&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\vec x$ 为 $p_0$ 和 $p_2$ 之间的向量，思考一下可知，这并不会被包含到情况二中。&lt;/p&gt;
&lt;p&gt;起点为 $p_0$ 和 $p_1$，$\vec x=(p_{2x}-p_{0x}, p_{2y}-p_{0y})$.&lt;/p&gt;
&lt;p&gt;$\rm{check}$ 方法类似，注意要保证 $n&gt;2$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;curx = p[2] - p[0];
ok = 1, cnt1 = 0, cnt2 = 0;
for(int t = 2; t &amp;#x3C; n; t++) {
    if(p[t] == p[0] + (cnt1 + 1) * curx) cnt1++;
    else if(p[t] == p[1] + (cnt2 + 1) * curx) cnt2++;
    else { ok = 0; break; }
}
if(ok) {
    z = cnt1, x = curx;
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;需特殊处理所有点共线的情况。在我的 &lt;a href=&quot;https://www.luogu.com.cn/paste/v3ozrf87&quot;&gt;这份AC代码&lt;/a&gt; 中，分开处理了所有点共线的部分，可以参考。&lt;/p&gt;
&lt;p&gt;下面的实现将特殊情况在旋转卡壳中一并处理，详见代码注释。&lt;/p&gt;
&lt;p&gt;由于旋转卡壳的循环性，下面的实现中有大量取模运算，并不利于阅读。&lt;strong&gt;分类讨论中的代码风格更适合阅读。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int n, z; Point x;
Point p_in[MAXN], p[MAXN], ch[MAXN];
void calc_x_and_z() { //求 x和 z
	bool collinear = true; //collinear = true &amp;#x3C;-&gt; 所有点都共线
	for(int i = 1; i &amp;#x3C; n; i++) {
		if((p[i - 1] - p[0]) * (p[i] - p[0]) != 0) {
			collinear = false;
		}
	}
	int siz, j, k; //siz凸包大小, j/k用于旋转卡壳
	int ok, tmp, cnt1, cnt2; //flag, 第一个不满足条件的点, 两个起点时用到的指针
	Point curx;
	if(collinear) { //所有点共线，特殊处理，认为所有点都在凸包里
		siz = n;
		memcpy(ch, p, sizeof(p)); sort(ch, ch + n);
	} else { //不都共线，求凸包
		siz = Convex_hull(n, p, ch), j = 1, k = 1;
		ch[siz] = ch[0]; ch[siz + 1] = ch[1];
	}
	for(int i = 0; i &amp;#x3C; siz; i++) { //旋转卡壳，(i, i+1)
		if(collinear) { //所有点共线时，只需要算一遍
			if(i &gt; 0) break;
			j = 0, k = n - 1;
		} else { //否则正常旋转卡壳
			j = k;
			while((ch[i] - ch[j+1]) * (ch[i+1] - ch[j+1]) &gt;
				(ch[i] - ch[j  ]) * (ch[i+1] - ch[j  ]))
				j = (j + 1) % siz;
			k = j;
			while((ch[i] - ch[k+1]) * (ch[i+1] - ch[k+1]) ==
				(ch[i] - ch[k  ]) * (ch[i+1] - ch[k  ]))
				k = (k + 1) % siz;
		}
		//[j, k]内的点均共线，所在直线与(i, i+1)平行
		if(k == j) continue;
		if(k == (j + 1) % siz) {
			if(1 &gt; z) z = 1, x = ch[j+1] - ch[j];
			continue;
		}
		//1. 只有一个起点，curx = ch[j+1] - ch[j]
		curx = ch[j + 1] - ch[j];
		ok = 1, cnt1 = 0;
		for(int t = (j + 1) % siz; t != (k + 1) % siz; t = (t + 1) % siz) {
			if(ch[t] != ch[j] + (cnt1 + 1) * curx) {
				ok = 0, tmp = t;
				break;
			} cnt1++;
		}
		if(ok) {
			if(cnt1 &gt; z) z = cnt1, x = curx;
			continue;
		}
		//2. 有两个起点 &amp;#x26; curx = ch[j+1] - ch[j]，起点为 ch[j] 和 ch[tmp]
		curx = ch[j + 1] - ch[j];
		ok = 1, cnt1 = 0, cnt2 = 0;
		for(int t = (j + 1) % siz; t != (k + 1) % siz; t = (t + 1) % siz) if(t != tmp) {
			if(ch[t] == ch[j] + (cnt1 + 1) * curx) cnt1++;
			else if(ch[t] == ch[tmp] + (cnt2 + 1) * curx) cnt2++;
			else { ok = 0; break; }
		}
		if(ok &amp;#x26;&amp;#x26; cnt1 == cnt2) {
			if(cnt1 &gt; z) z = cnt1, x = curx;
			continue;
		}
		//3. 两个起点 &amp;#x26; curx = ch[j+2] - ch[j]，起点为 ch[j] 和 ch[j+1]
		curx = ch[j + 2] - ch[j];
		ok = 1, cnt1 = 0, cnt2 = 0;
		for(int t = (j + 2) % siz; t != (k + 1) % siz; t = (t + 1) % siz) {
			if(ch[t] == ch[j] + (cnt1 + 1) * curx) cnt1++;
			else if(ch[t] == ch[j + 1] + (cnt2 + 1) * curx) cnt2++;
			else { ok = 0; break; }
		}
		if(ok &amp;#x26;&amp;#x26; cnt1 == cnt2) {
			if(cnt1 &gt; z) z = cnt1, x = curx;
			continue;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;其他部分的代码实现&lt;/h1&gt;
&lt;p&gt;两个主要函数的代码，在上文已经给出。&lt;/p&gt;
&lt;p&gt;这里&lt;strong&gt;仅包含&lt;/strong&gt;：向量类的定义、凸包的求法、主函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e5 + 10, inf = 0x3f3f3f3f;
struct Point{ //向量类
	int x, y;
	Point(){};
	Point(int a, int b): x(a), y(b) {}
	Point(Point a, Point b): x(b.x - a.x), y(b.y - a.y) {}
	friend Point operator + (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x + b.x, a.y + b.y);
	}
	friend Point operator - (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x - b.x, a.y - b.y);
	}
	friend bool operator == (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x == b.x &amp;#x26;&amp;#x26; a.y == b.y;
	}
	friend bool operator != (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x != b.x || a.y != b.y;
	}
	friend Point operator * (const int &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a * b.x, a * b.y);
	}
	friend Point operator * (const Point &amp;#x26;a, const int &amp;#x26;b) {
		return Point(a.x * b, a.y * b);
	}
	friend int operator * (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x * b.y - a.y * b.x;
	}
	friend int operator &amp;#x26; (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x * b.x + a.y * b.y;
	}
	friend bool operator &amp;#x3C; (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return (a.x &amp;#x3C; b.x) || (a.x == b.x &amp;#x26;&amp;#x26; a.y &amp;#x3C; b.y);
	}
};

inline bool check(Point s1, Point s2, Point p) {
	return Point(s2, s1) * Point(s1, p) &gt;= 0; //凸包保留共线的点
}
int Convex_hull(int n, Point *p, Point *ret) { //求二维凸包，返回值为凸包大小
	sort(p, p + n);
	int top = -1;
	for (int i = 0; i &amp;#x3C; n; i++) {
		while (top &gt; 0 &amp;#x26;&amp;#x26; !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	int k = top;
	for (int i = n - 2; i &gt;= 0; i--) {
		while (top &gt; k &amp;#x26;&amp;#x26; !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	return top;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;signed main() {
	n = read();
	for(int i = 0; i &amp;#x3C; n; i++) p_in[i].x = read(), p_in[i].y = read();
	for(int i = 0; i &amp;#x3C; n; i++) p[i] = p_in[i];

	calc_x_and_z();
	calc_b();

	printf(&quot;%lld\n&quot;, (long long)b.size());
	for(int i = 0; i &amp;#x3C; b.size(); i++) printf(&quot;%lld &quot;, b[i] + 1); putchar(&apos;\n&apos;);
	printf(&quot;%lld %lld\n%lld\n&quot;, x.x, x.y, z);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces–1623D】Robot Cleaner Revisit</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1623d</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1623d</guid><description>概率题</description><pubDate>Mon, 03 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://codeforces.com/contest/1623/problem/D&quot;&gt;题目链接 - CodeForces&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;地板有 $n$ 行 $m$ 列，一个机器人被安置在坐标 $(r_b, c_b)$，还有一个障碍位于 $(r_d, c_d)$。&lt;/p&gt;
&lt;p&gt;每过一秒，机器人的坐标会 $(r, c)\rightarrow (r+d_r, c+d_c)$。&lt;/p&gt;
&lt;p&gt;初始时 $d_r=d_c=1$，当机器人到达边界后，$d_r,d_c$ 会变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若机器人碰到上/下边界（$r=1$ 或 $r=n$），下一秒 $d_r\leftarrow -d_r$，&lt;/li&gt;
&lt;li&gt;若机器人到达左/右边界（$c=1$ 或 $c=m$），下一秒 $d_c\leftarrow -d_c$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一秒内，位于 $(r,c)$ 的机器人有 $\frac{p}{100}$ 的概率，成功清理掉位于第 $r$ 行和第 $c$ 列的障碍，&lt;strong&gt;之后&lt;/strong&gt;，它将按照上面的规则进行移动。&lt;/p&gt;
&lt;p&gt;若每次清理的结果相互独立，求清理掉障碍物 $(r_d, c_d)$ 所需的期望时间。&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;参考了由 &lt;a href=&quot;https://codeforces.com/profile/darkkcyan&quot;&gt;darkkcyan&lt;/a&gt; 提供的 &lt;a href=&quot;https://codeforces.com/blog/entry/98463&quot;&gt;Codeforces Round #763 (Div. 2) Editorial&lt;/a&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，设 $\overline{p}=1-\frac{p}{100}$，代表不成功的概率。可以发现，机器人的轨迹会形成一个循环。&lt;/p&gt;
&lt;h4&gt;样例一&lt;/h4&gt;
&lt;p&gt;从样例一开始分析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wordpress-blog-1301627340.cos.ap-beijing.myqcloud.com/2022/01/03/46b9c9979c1db.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;循环共两步，&lt;strong&gt;设 $x_1$ 表示机器人从 $(1,1)$ 出发的答案，$x_2$ 表示从 $(2,2)$ 出发的答案&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当位于 $(1, 1)$ 时，&lt;strong&gt;如果成功&lt;/strong&gt;，花费时间 $0s$；&lt;strong&gt;若否&lt;/strong&gt;，走到 $(2,2)$ 需要 $1s$，之后还需要 $x_2$，所以有：
$$
\begin{cases}
x_1=p\cdot 0 + \overline{p}(1+x_2)=\overline{p}(1+x_2)\
x_2=p\cdot 0 + \overline{p}(1+x_1)=\overline{p}(1+x_1)\
\end{cases}
$$
联立一下，得到关于 $x_1$ 的方程：
$$
x_1=\overline{p}(1+\overline{p}(1+x_1))
$$
逐层展开求解，$x_1$ 就是题目所求的答案。&lt;/p&gt;
&lt;h4&gt;样例二&lt;/h4&gt;
&lt;p&gt;再看样例二。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wordpress-blog-1301627340.cos.ap-beijing.myqcloud.com/2022/01/03/380a00f5fd09e.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;循环共四步：$(1,1)\rightarrow (2,2)\rightarrow (1,3)\rightarrow (2,2)$，&lt;strong&gt;其中 $(1,1)$ 不可能清除障碍&lt;/strong&gt;，其余位置均有可能。&lt;/p&gt;
&lt;p&gt;同理，设 $x_1$ 表示从 $(1,1)$ 出发的答案，$x_i$ 表示从循环第 $i$ 步出发的答案。（注意 $x_2\neq x_4$，它们的方向不同）&lt;/p&gt;
&lt;p&gt;与样例一类似地，有：
$$
\begin{cases}
x_1 = 1 + x_2\
x_2 = \overline{p}(1 + x_3)\
x_3 = \overline{p}(1 + x_4)\
x_4 = \overline{p}(1 + x_1)\
\end{cases}
$$
联立，得到关于 $x_1$ 的方程：
$$
x_1=1 + \overline{p}(1 + \overline{p}(1 + \overline{p}(1 + x_1)))
$$&lt;/p&gt;
&lt;h4&gt;做法&lt;/h4&gt;
&lt;p&gt;若我们已知循环共有 $k$ 个点，且已知其中每个点的坐标，则可以列出像上面那样的方程组。&lt;/p&gt;
&lt;p&gt;联立后的方程形如：
$$
x=a_1(1+a_2(1+a_3(1+\cdots a_k(1+x)\cdots)))
$$
其中 $a_i\in{1,\overline{p}}$，是系数。&lt;strong&gt;当循环中第 $i$ 步可以清除障碍时 $a_i=\overline{p}$，否则 $a_i=1$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;由于数据范围 $4\leq n\cdot m\leq 10^5$，我们可以直接&lt;strong&gt;暴力搜索找到循环&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;求解方程时，考虑从内向外逐层展开，过程中记录：常数项 $u$、一次项系数 $v$。&lt;/p&gt;
&lt;p&gt;初始 $u=0$，$v=1$，&lt;strong&gt;每次展开时先算出 $a$，之后 $\begin{cases}v\leftarrow a\cdot v\u\leftarrow a\cdot (u+1)\end{cases}$ 就可以了&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;循环长度&lt;/h4&gt;
&lt;p&gt;我们把机器人的移动，分解在 $x/y$ 方向分别看。在一维的投影上，它们的周期分别为 $2(n-1)$ 和 $2(m-1)$.、&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wordpress-blog-1301627340.cos.ap-beijing.myqcloud.com/2022/01/03/bb435997809dc.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因此，$4(n-1)(m-1)$ 一定是 $\operatorname{lcm}(2(n-1), 2(m-1))$ 的整数倍，&lt;strong&gt;是循环长度的整数倍&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以不必通过暴力搜索找到循环，对于所有数据都直接迭代 $4(n-1)(m-1)$ 次即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;注意倒序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int mod = 1e9 + 7;
inline int power(int x, int k) {
	int ret = 1;
	while(k) { if(k &amp;#x26; 1) ret = ret * x % mod; x = x * x % mod; k &gt;&gt;= 1; }
	return ret;
}
inline int inv(int x) { return power(x % mod, mod - 2); }
int T, n, m, ax, ay, bx, by, dx, dy, p, pp;
signed main() {
	cin &gt;&gt; T;
	while(T--) {
		cin &gt;&gt; n &gt;&gt; m &gt;&gt; ax &gt;&gt; ay &gt;&gt; bx &gt;&gt; by &gt;&gt; p;
		pp = (100 - p) * inv(100) % mod;
		p = p * inv(100) % mod;
		int u = 0, v = 1;
		dx = dy = -1; //倒序
		for(int cas = 1; cas &amp;#x3C;= 4 * (n - 1) * (m - 1); cas++) { //迭代4(n-1)(m-1)次
			if(ax + dx &gt; n || ax + dx &amp;#x3C; 1) dx = -dx;
			if(ay + dy &gt; m || ay + dy &amp;#x3C; 1) dy = -dy;
			ax += dx; ay += dy;
			u = (u + 1) % mod;
			if(ax == bx || ay == by) u = u * pp % mod, v = v * pp % mod;
		}
		cout &amp;#x3C;&amp;#x3C; u * inv(mod + 1 - v) % mod &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>莫比乌斯反演 &amp; 狄利克雷卷积</title><link>https://www.tonyyin0418.com/blog/oi-notes/mobius-inversion</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/mobius-inversion</guid><description>莫比乌斯反演、狄利克雷卷积与数论函数。</description><pubDate>Sun, 26 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;莫比乌斯反演 &amp;#x26; 狄利克雷卷积&lt;/p&gt;
&lt;h1&gt;P1829 [国家集训队]Crash的数字表格&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;=\sum_{i=1}^{n}\sum_{j=1}^{m}\operatorname{lcm}(i, j)\
&amp;#x26;=\sum_{i=1}^{n}\sum_{j=1}^{m}\frac{i\times j}{\operatorname{gcd}(i, j)}\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}\sum_{i=1}^n\sum_{j=1}^m\frac{i\times j}{d} [\gcd(i,j)=d]\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}\frac{(i\cdot d)\times (j\cdot d)}{d} [\gcd(i,j)=1]\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}d\cdot\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}i\times j[\gcd(i,j)=1]\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}d\cdot\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}i\times j\sum_{x\mid \gcd(i, j)}\mu(x)\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}d\sum_{x=1}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x)\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}(i\times j)[x\mid i][x\mid j]\
&amp;#x26;=\sum_{d=1}^{\min(n,m)}d\sum_{x=1}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x)\cdot x^2\sum_{i=1}^{\lfloor\frac{n}{dx}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{dx}\rfloor}(i\times j)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;前面两个 $\sum$ 只需预处理 $\mu(x)$ 的前缀和，最后两个 $\sum$ 可以用等差数列 $\mathcal{O}(1)$ 算出，于是答案可线性计算。
$$
\begin{aligned}
g(n,m) &amp;#x26;= \sum_{i=1}^n\sum_{j=1}^mi\times j\
&amp;#x26;=\frac{n(n+1)}{2}\frac{m(m+1)}{2}
\end{aligned}
$$
整除分块，复杂度为 $\sum\limits_{i=1}^n \sqrt\frac{n}{i}$，计算可知，当 $n=10^7$ 时，左边式子的值 $&amp;#x3C;2\times 10^7$，可以通过。&lt;/p&gt;
&lt;p&gt;注意取模、逆元。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int d = 1; d &amp;#x3C;= min(n, m); d++) {
    int t = min(n / d, m / d), tmp = 0;
    for(int l = 1, r; l &amp;#x3C;= t; l = r + 1) {
        r = min((n / d) / ((n / d) / l), (m / d) / ((m / d) / l));
        (tmp += (mu[r] - mu[l - 1]) % mod * g(n / d / l, m / d / l) % mod) %= mod;
    }
    ans = (ans + tmp * d % mod) % mod;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;P3312 [SDOI2014]数表&lt;/h1&gt;
&lt;p&gt;先忽略 $a$ 的限制？&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^{n}\sum_{j=1}^{m}\sum_{x=1}^{\min(n, m)}x[x\mid i][x\mid j]\
&amp;#x26;= \sum_{x=1}^{\min(n, m)}x\lfloor\frac{n}{x}\rfloor\lfloor\frac{m}{x}\rfloor\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;这样虽然形式简单，但并不能继续进行…&lt;/p&gt;
&lt;p&gt;换成下面的形式：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^{n}\sum_{j=1}^{m}\sigma(\gcd(i,j))\
&amp;#x26;= \sum_{d=1}^{n} \sum_{i=1}^{n}\sum_{j=1}^{m} \sigma(d)[\gcd(i,j)=d]\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}[\gcd(i,j)=1]\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}\sum_{x\mid \gcd(i,j)}\mu(x)\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}\sum_{x}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x)[x\mid i][x\mid j]\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{x}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x) \sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}[x\mid i]\sum_{j=1}^{\lfloor\frac{m}{d}\rfloor}[x\mid j]\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{x}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x) \sum_{i=1}^{\lfloor\frac{n}{dx}\rfloor}1\sum_{j=1}^{\lfloor\frac{m}{dx}\rfloor}1\
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{x}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x) \lfloor\frac{n}{dx}\rfloor\lfloor\frac{m}{dx}\rfloor\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;记 $t=dx$，则可以更改枚举顺序，继续简化：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{d=1}^{\min(n,m)} \sigma(d) \sum_{x}^{\min(\lfloor\frac{n}{d}\rfloor, \lfloor\frac{m}{d}\rfloor)}\mu(x) \lfloor\frac{n}{dx}\rfloor\lfloor\frac{m}{dx}\rfloor\
&amp;#x26;= \sum_{t=1}^{n} \lfloor\frac{n}{t}\rfloor\lfloor\frac{m}{t}\rfloor \sum_{d\mid t} \sigma(d) \mu(\frac{t}{d})
\end{aligned}
$$
观察推导过程，可知当 $\sigma(d)\leq a$ 时才会对答案有贡献。&lt;/p&gt;
&lt;p&gt;设 $g(t)=\sum_{d\mid t} \sigma(d) \mu(\frac{t}{d})$.&lt;/p&gt;
&lt;p&gt;以 $a$ 为关键字，从小到大枚举询问。$a$ 变大时，有一些 $g(t)$ 的值需要增加，可以直接枚举新加入的数的所有倍数，来找到所有被贡献的 $t$.&lt;/p&gt;
&lt;p&gt;用树状数组维护 $g(t)$，时间复杂度为 $\mathcal{O}(q\sqrt n\log n+n\log^2 n)$.&lt;/p&gt;
&lt;h1&gt;P3455 [POI2007]ZAP-Queries&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^{a}\sum_{j=1}^{b}[\gcd(i, j) = d]\
&amp;#x26;= \sum_{i=1}^{\lfloor\frac{a}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{b}{d}\rfloor} [\gcd(i, j) = 1]\
&amp;#x26;= \sum_{i=1}^{\lfloor\frac{a}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{b}{d}\rfloor}\sum_{d\mid \gcd(i, j)}\mu(d)\
&amp;#x26;= \sum_{i=1}^{\lfloor\frac{a}{d}\rfloor}\sum_{j=1}^{\lfloor\frac{b}{d}\rfloor}\sum_{x=1}^{\min(\lfloor\frac{a}{d}\rfloor,\lfloor\frac{b}{d}\rfloor)} \mu(x)[d\mid i][d\mid j]\
&amp;#x26;= \sum_{x=1}^{\min(\lfloor\frac{a}{d}\rfloor, \lfloor\frac{b}{d}\rfloor)}\mu(x)\sum_{i=1}^{\lfloor\frac{a}{d}\rfloor}[d\mid i]\sum_{j=1}^{\lfloor\frac{b}{d}\rfloor}[d\mid j]\
&amp;#x26;= \sum_{x=1}^{\min(\lfloor\frac{a}{d}\rfloor, \lfloor\frac{b}{d}\rfloor)}\mu(x)\lfloor\frac{a}{xd}\rfloor\lfloor\frac{b}{xd}\rfloor
\end{aligned}
$$&lt;/p&gt;
&lt;h1&gt;P3172 [CQOI2015]选数&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{a_1=L}^{h}\sum_{a_2=L}^{h}\cdots\sum_{a_n=L}^{h}[\gcd(a_1,a_2\cdots, a_n)=k]\
&amp;#x26;= \sum_{a_1=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\sum_{a_2=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\cdots\sum_{a_n=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}[\gcd(a_1,a_2\cdots a_n)=1]\
&amp;#x26;= \sum_{a_1=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\sum_{a_2=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\cdots\sum_{a_n=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\sum_{d\mid \gcd(a_1,a_2\cdots a_n)}\mu (d)\
&amp;#x26;= \sum_{a_1=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\sum_{a_2=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\cdots\sum_{a_n=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}\cdot \sum_{d=1}^{\lfloor\frac{h}{k}\rfloor}\mu (d)[d\mid a_1][d\mid a_2]\cdots [d\mid a_n]\
&amp;#x26;= \sum_{d=1}^{\lfloor\frac{h}{k}\rfloor}\mu (d)\cdot \sum_{a_1=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_1]\cdot \sum_{a_2=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_2]\cdots\sum_{a_n=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_n]\
&amp;#x26;= \sum_{d=1}^{\lfloor\frac{h}{k}\rfloor}\mu (d)\cdot \sum_{a_1=\lfloor\frac{l-1}{k}\rfloor+1}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_1]\cdot \sum_{a_2=\lfloor\frac{l-1}{k}\rfloor+1}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_2]\cdots\sum_{a_n=\lfloor\frac{l-1}{k}\rfloor+1}^{\lfloor\frac{h}{k}\rfloor}[d\mid a_n]\
&amp;#x26;= \sum_{d=1}^{\lfloor\frac{h}{k}\rfloor}\mu (d)\cdot (\lfloor\frac{h}{kd}\rfloor - \lfloor\frac{l-1}{kd}\rfloor)^n
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;杜教筛、整除分块。&lt;/p&gt;
&lt;h1&gt;P6810 「MCOI-02」Convex Hull 凸包&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= 	\sum_{i=1}^{n}\sum_{j=1}^{m}\tau(i)\tau(j)\tau(\gcd(i, j))\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{i=1}^{n}\sum_{j=1}^{m}\tau(i)\tau(j)[\gcd(i, j)=x]\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{i=1}^{\lfloor\frac{n}{x}\rfloor}
\sum_{j=1}^{\lfloor\frac{m}{x}\rfloor}\tau(xi)\tau(xj)[\gcd(i, j)=1]\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{i=1}^{\lfloor\frac{n}{x}\rfloor}
\sum_{j=1}^{\lfloor\frac{m}{x}\rfloor}\tau(xi)\tau(xj)
\sum_{d\mid \gcd(i,j)}\mu(d)\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{i=1}^{\lfloor\frac{n}{x}\rfloor}
\sum_{j=1}^{\lfloor\frac{m}{x}\rfloor}\tau(xi)\tau(xj)
\sum_{d=1}^{\lfloor\frac{\min(n, m)}{x}\rfloor}\mu(d)[d\mid i][d\mid j]\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{d=1}^{\lfloor\frac{\min(n, m)}{x}\rfloor}\mu(d)
\sum_{i=1}^{\lfloor\frac{n}{x}\rfloor}[d\mid i]\tau(xi)
\sum_{j=1}^{\lfloor\frac{m}{x}\rfloor}[d\mid j]\tau(xj)\
&amp;#x26;= 	\sum_{x=1}^{\min(n, m)}\tau(x)
\sum_{d=1}^{\lfloor\frac{\min(n, m)}{x}\rfloor}\mu(d)
\sum_{i=1}^{\lfloor\frac{n}{xd}\rfloor}\tau(xid)
\sum_{j=1}^{\lfloor\frac{m}{xd}\rfloor}\tau(xjd)
%&amp;#x26;=  \sum_{x=1}^{\min(n, m)}\tau^3(x)
%    \sum_{d=1}^{\lfloor\frac{\min(n, m)}{x}\rfloor}\mu(d)\tau^2(d)
%    \sum_{i=1}^{\lfloor\frac{n}{xd}\rfloor}\tau(i)
%    \sum_{j=1}^{\lfloor\frac{m}{xd}\rfloor}\tau(j)\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;对于 $\sum\limits_{x=1}^{\min(n, m)}\tau(x)
\sum\limits_{d=1}^{\lfloor\frac{\min(n, m)}{x}\rfloor}\mu(d)$，暴力循环枚举即可。复杂度用调和级数算，是 $\mathcal{O}(n\ln n)$.&lt;/p&gt;
&lt;p&gt;对于 $\sum\limits_{i=1}^{\lfloor\frac{n}{xd}\rfloor}\tau(xid)
\sum\limits_{j=1}^{\lfloor\frac{m}{xd}\rfloor}\tau(xjd)$，可以在预处理后 $\mathcal{O}(1)$ 计算。&lt;/p&gt;
&lt;p&gt;先线性筛出所有的 $\tau(x)$，预处理出 $s(x,y)=\sum\limits_{i=1}^{y} \tau(xi)$.&lt;/p&gt;
&lt;p&gt;若 $x$ 固定，则 $y\in[1, \lfloor\frac{\min(n, m)}{x}\rfloor]$，所以预处理的复杂度也是 $\mathcal{O}(n\ln n)$ 级别的。&lt;/p&gt;
&lt;p&gt;这tm会被卡常。&lt;/p&gt;
&lt;p&gt;所以有更简单的推导方法：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^{n}\sum_{j=1}^{m}\tau(i)\tau(j)\tau(\gcd(i,j))\
&amp;#x26;= \sum_{i=1}^{n}\sum_{j=1}^{m}\tau(i)\tau(j)\sum_{d\mid \gcd(i,j)}1\
&amp;#x26;= \sum_{d=1}^{n}\cdot \sum_{i=1}^{n}[d\mid i]\tau(i)\cdot\sum_{j=1}^{n}[d\mid j]\tau(j)\
&amp;#x26;= \sum_{d=1}^{n}\cdot \sum_{i=1}^{\lfloor\frac{n}{d}\rfloor} \tau(id)\cdot \sum_{j=1}^{\lfloor\frac{m}{d}\rfloor} \tau(jd)
\end{aligned}
$$
预处理出 $\sum\limits_{i=1}^{\lfloor\frac{n}{d}\rfloor} \tau(id)$ 即可。&lt;/p&gt;
&lt;h1&gt;P3704 [SDOI2017]数字表格&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans} &amp;#x26;= \prod_{i=1}^n \prod_{j=1}^m f_{\gcd(i,j)}\
&amp;#x26;= \prod_{d=1}^{\min(n, m)} f_d ^{\sum_{i=1}^n \sum_{j=1}^m [\gcd(i,j)=d]}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;其中：
$$
\begin{aligned}
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^m [\gcd(i,j)=d]\
&amp;#x26;= \sum_{x=1}^{\min(n/d,m/d)} \mu(x) \sum_{i=1}^{n/d} [x\mid i] \sum_{j=1}^{m/d} [x\mid j]\
&amp;#x26;= \sum_{x=1}^{\min(n/d,m/d)} \mu(x) (n/dx)(m/dx)\
\end{aligned}
$$
继续：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \prod_{d=1}^{\min(n, m)} {f_d}^{\sum\limits_{i=1}^n \sum\limits &lt;em&gt;{j=1}^m [\gcd(i,j)=d]}\
&amp;#x26;= \prod&lt;/em&gt;{d=1}^{\min(n, m)} {f_d}^{\sum\limits_{x=1}^{\min(n/d,m/d)} \mu(x) (n/dx)(m/dx)}\
\end{aligned}
$$
常见套路，设 $t=dx$，则：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \prod_{d=1}^{\min(n, m)} {f_d}^{\sum\limits_{x=1}^{\min(n/d,m/d)} \mu(x) (n/dx)(m/dx)}\
&amp;#x26;= \prod_{t=1}^{\min(n, m)} \prod_{d\mid t} {f_d}^{\mu(t/d) (n/t)(m/t)}\
&amp;#x26;= \prod_{t=1}^{\min(n, m)} \prod_{d\mid t} {({f_d}^{\mu(t/d)})}^{(n/t)(m/t)}\
\end{aligned}
$$
设 $g_t=\prod_{d\mid t} {f_d}^{\mu (t/d)}$，则：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \prod_{t=1}^{\min(n, m)} \prod_{d\mid t} {({f_d}^{\mu(t/d)})}^{(n/t)(m/t)}\
&amp;#x26;= \prod_{t=1}^{\min(n, m)} {g_t}^{(n/t)(m/t)}\
\end{aligned}
$$
枚举每个 $d$，预处理出 $g_t$ 数组，复杂度为调和级数 $\frac{n}{1}+\frac{n}{2}+\cdots+\frac{n}{n}=n\ln n$.&lt;/p&gt;
&lt;p&gt;多测，对每次询问，利用数论分块计算 $\operatorname{Ans}$，需要先算出 $g_t$ 的前缀积。&lt;/p&gt;
&lt;p&gt;时间复杂度为 $\mathcal{O}(t\sqrt n+n\ln n)$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：代码取模时，指数部分要 $\bmod (p-1)$ 而不是 $\bmod p$，由费马小定理知 $a^{p-1} \equiv a^0\pmod p$.&lt;/p&gt;
&lt;h1&gt;P2257 YY的GCD&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^m \sum_{p=1}^{\min(n,m)} [\gcd(i,j)=p] ,p\in \mathrm{prime}\
&amp;#x26;= \sum_{p=1}^{\min(n,m)} \sum_{i=1}^{\lfloor\frac{n}{p}\rfloor} \sum_{j=1}^{\lfloor\frac{m}{p}\rfloor} [\gcd(i,j)=1] ,p\in \mathrm{prime}\
&amp;#x26;= \sum_{p=1}^{\min(n,m)} \sum_{i=1}^{\lfloor\frac{n}{p}\rfloor} \sum_{j=1}^{\lfloor\frac{m}{p}\rfloor} \sum_{x\mid \gcd(i,j)} \mu(x),p\in \mathrm{prime}\
&amp;#x26;= \sum_{p=1}^{\min(n,m)} \sum_{x=1}^{\min(\lfloor\frac{n}{p}\rfloor, \lfloor\frac{m}{p}\rfloor)}\mu(x)\lfloor\frac{n}{px}\rfloor\cdot \lfloor\frac{m}{px}\rfloor,p\in \mathrm{prime}\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;设 $t=px$，
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{p=1}^{\min(n,m)} \sum_{x=1}^{\min(\lfloor\frac{n}{p}\rfloor, \lfloor\frac{m}{p}\rfloor)}\mu(x)\lfloor\frac{n}{px}\rfloor \lfloor\frac{m}{px}\rfloor,p\in \mathrm{prime}\
&amp;#x26;= \sum_{t=1}^{\min(n,m)} \lfloor\frac{n}{t}\rfloor \lfloor\frac{m}{t}\rfloor\sum_{x\mid t}\mu(x),\frac{t}{x}\in \mathrm{prime}\
&amp;#x26;= \sum_{t=1}^{\min(n,m)} \lfloor\frac{n}{t}\rfloor \lfloor\frac{m}{t}\rfloor\sum_{x\mid t}\mu(\frac{t}{x}),x\in \mathrm{prime}\
\end{aligned}
$$&lt;/p&gt;
&lt;h1&gt;P3911 最小公倍数之和&lt;/h1&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^n \operatorname{lcm}(A_i, A_j)\
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^n \frac{A_iA_j}{\gcd(A_i, A_j)}\
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^n \sum_{d=1}^m \frac{A_iA_j}{d}[\gcd(A_i, A_j)=d]\
&amp;#x26;= \sum_{i=1}^n \frac{A_i}{d} \sum_{j=1}^n \frac{A_j}{d} \sum_{d=1}^m \frac{1}{d}[\gcd(A_i, A_j)=1]\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;上面是错的。&lt;/p&gt;
&lt;p&gt;设 $c_i$ 为 $i$ 在 $A$ 中出现的次数，值域为 $[1,m]$，则：
$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{i=1}^n \sum_{j=1}^n \operatorname{lcm}(A_i, A_j)\
&amp;#x26;= \sum_{i=1}^m \sum_{j=1}^m \operatorname{lcm}(i, j)\cdot c_ic_j\
&amp;#x26;= \sum_{i=1}^m \sum_{j=1}^m \frac{i\cdot j}{\gcd(i,j)}\cdot c_ic_j\
&amp;#x26;= \sum_{i=1}^m i\cdot c_i \sum_{j=1}^m j\cdot c_j \sum_{d=1}^m \frac{1}{d}[\gcd(i,j)=d]\
&amp;#x26;= \sum_{d=1}^m \frac{1}{d} \sum_{i=1}^{m/d} {di}\cdot c_{di} \sum_{j=1}^{m/d} {dj}\cdot c_{dj} [\gcd(i,j)=1]\
&amp;#x26;= \sum_{d=1}^m \frac{1}{d} \sum_{i=1}^{m/d} {di}\cdot c_{di} \sum_{j=1}^{m/d} {dj}\cdot c_{dj}\sum_{x=1}^{m/d} \mu(x)\
&amp;#x26;= \sum_{d=1}^m \frac{1}{d} \sum_{x=1}^{m/d} \mu(x) \sum_{i=1}^{m/dx} {dxi}\cdot c_{dxi} \sum_{j=1}^{m/dx} {dxj}\cdot c_{dxj}\
\end{aligned}
$$
预处理 $f(t)=\sum\limits_{i=1}^{m/t} ti\cdot c_{ti}$.&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{Ans}
&amp;#x26;= \sum_{d=1}^m \frac{1}{d} \sum_{x=1}^{m/d} \mu(x) \sum_{i=1}^{m/dx} {dxi}\cdot c_{dxi} \sum_{j=1}^{m/dx} {dxj}\cdot c_{dxj}\
&amp;#x26;= \sum_{d=1}^m \frac{1}{d} \sum_{x=1}^{m/d} \mu(x) [f(dx)]^2\
\end{aligned}
$$&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P4492】[HAOI2018]苹果树</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4492</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4492</guid><description>算法题解</description><pubDate>Sun, 26 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;现在有 $n$ 次操作，每次从当前二叉树上的所有空位（节点的左/右儿子）中等概率选择一个，插入新点。&lt;/p&gt;
&lt;p&gt;对最终 $n$ 个点的二叉树，求：&lt;strong&gt;两两节点距离之和的期望&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;加入第 $i$ 个点时，方案数为 $i$，根据乘法原理，一共有 $n!$ 种可能的二叉树。只需要&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;。形式化地，枚举 $i\in[2, n]$，则当前边是 $\langle i,\operatorname{father}&lt;em&gt;i\rangle$，其被经过的次数为 $\operatorname{size}&lt;/em&gt;{i}(n - \operatorname{size}_i)$.&lt;/p&gt;
&lt;p&gt;由于 $\operatorname{size}_i$ 并不知道，所以&lt;strong&gt;再枚举子树大小&lt;/strong&gt; $\operatorname{size}_i\in[1, n-i+1]$.&lt;/p&gt;
&lt;p&gt;接下来我们要&lt;strong&gt;确定树的形态&lt;/strong&gt;，求满足 $\operatorname{size}&lt;em&gt;i$ 限制的方案数，按照插入的顺序分别考虑。
$$
\underbrace{1\quad2\quad3\quad \cdots\quad i-2\quad i-1\quad i}&lt;/em&gt;{A}\quad\underbrace{ i+1\quad i+2\quad \cdots\quad n-2\quad n-1\quad n}_{B}
$$&lt;/p&gt;
&lt;h4&gt;$A$ 段&lt;/h4&gt;
&lt;p&gt;包含前 $i$ 个点，这些点和 $i$ 的子树没什么关系，都可以随便插入，共有 $i!$ 种方案。&lt;/p&gt;
&lt;h4&gt;$B$ 段&lt;/h4&gt;
&lt;p&gt;我们首先要从中&lt;strong&gt;选出 $\operatorname{size}_i-1$ 个节点插入 $i$ 的子树&lt;/strong&gt;，共有 $\dbinom{n-i}{\operatorname{size}_i-1}$ 种方案。&lt;/p&gt;
&lt;p&gt;这 $\operatorname{size_i}$ 个点是可以以任意顺序组成二叉树的，共有 $(\operatorname{size}_i)!$ 种方案。&lt;/p&gt;
&lt;p&gt;对于 $B$ 中剩下的 $n-i-\operatorname{size}_i+1$ 个点，它们&lt;strong&gt;都不可以放在 $i$ 的子树内&lt;/strong&gt;，下面计算这部分的方案数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/12/26/6b7b9c0c49775.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图中，$i=9$，可以发现此时共有 $8=i-1$ 个位置可供选择。&lt;/p&gt;
&lt;p&gt;并且，每当我们插入一个节点，可供选择的位置就会增加 $1$。&lt;/p&gt;
&lt;p&gt;所以把 $B$ 中剩下的点插入的方案数为：$(i-1)i(i+1)\cdots(n-\operatorname{size}_i-1)=\dfrac{(n-\operatorname{size}_i-1)!}{(i-2)!}$.&lt;/p&gt;
&lt;p&gt;把上面的方案数都乘起来，再乘上 $\langle i,\operatorname{father}&lt;em&gt;i\rangle$ 这条边的贡献，得到答案的表达式：
$$
\operatorname{Ans} = \sum&lt;/em&gt;{i=2}^{n}\sum_{\operatorname{size}_i=1}^{n-i+1} \operatorname{size}_i(n - \operatorname{size}_i) \cdot i!\cdot \binom{n-i}{\operatorname{size}_i-1}(\operatorname{size}&lt;em&gt;i)! \cdot \frac{(n-\operatorname{size}&lt;em&gt;i-1)!}{(i-2)!}
$$
把 $i!$ 与 $(i-2)!$ 合并，得到：
$$
\operatorname{Ans} = \sum&lt;/em&gt;{i=2}^{n}\sum&lt;/em&gt;{\operatorname{size}_i=1}^{n-i+1} \operatorname{size}_i(n - \operatorname{size}_i) \cdot \binom{n-i}{\operatorname{size}_i-1}(\operatorname{size}_i)! \cdot {(n-\operatorname{size}_i-1)!}\cdot i(i-1)
$$
预处理阶乘和组合数，$\mathcal{O}(n^2)$ 计算即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;为了提高可读性，下面代码中&lt;strong&gt;统计答案部分的取模操作被省去&lt;/strong&gt;，直接提交将获得 $\rm{20pts}$ 的好成绩。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 2e3 + 10;
int N, P;
int fac[MAXN], C[MAXN][MAXN], ans;
signed main() {
	cin &gt;&gt; N &gt;&gt; P;
	fac[0] = 1;
	for(int i = 1; i &amp;#x3C;= N; i++) fac[i] = fac[i - 1] * i % P;
	for(int i = 0; i &amp;#x3C;= N; i++) C[i][0] = 1;
	for(int i = 1; i &amp;#x3C;= N; i++) 
        for(int j = 1; j &amp;#x3C;= i; j++) 
        	C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
	for(int i = 2; i &amp;#x3C;= N; i++) {
        for(int siz = 1; siz &amp;#x3C;= N - i + 1; siz++) {
			ans += C[N - i][siz - 1] * siz * (N - siz) * fac[siz] * fac[N - siz - 1] * i * (i - 1);
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces-1446D2】Frequency Problem (Hard Version)</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1446d2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1446d2</guid><description>根号分治好题。 </description><pubDate>Tue, 21 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;根号分治好题。&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给出一个序列 $a_1, a_2\cdots, a_n$，求最长的子段，使得其中有至少两个出现次数最多的元素。&lt;/p&gt;
&lt;p&gt;输出子段的长度，$1\leq n\leq 2\times 10^5$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h4&gt;引理及证明&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;众数：一个序列中出现次数最多的数，可以有多个。&lt;/p&gt;
&lt;/blockquote&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;记 $x$ 是原序列的众数，&lt;strong&gt;假设存在&lt;/strong&gt;子段 $a_l, a_{l+1}, \cdots, a_r$ 是题目所求的最长子段，且子段的众数不包含 $x$.&lt;/p&gt;
&lt;p&gt;根据众数的定义，在 $a_{[1, l-1]}$ 和 $a_{[r+1, n]}$ 中一定还包含 $x$。不妨设左侧包含 $x$，即 $\exists a_p=x, p\in[1, l-1]$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考虑将子段 $a_{[l, r]}$ 向两侧扩展，把 $a_p$ 包含进去&lt;/strong&gt;，这样使子段长度变大，下面说明一定存在方案可以包含 $a_p$.&lt;/p&gt;
&lt;p&gt;把 $a_{[l, r]}$ 扩展为 $a_{[p,r]}$ 后，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若其众数的出现次数&lt;strong&gt;不变&lt;/strong&gt;，则 $a_{[p,r]}$ 即为更优答案。&lt;/li&gt;
&lt;li&gt;若其众数的出现次数&lt;strong&gt;增加&lt;/strong&gt;，那么我们继续扩展，使更多的 $x$ 被包含进去。由于 $x$ 是整个序列中出现次数最多的数，所以把 $x$ 一个一个加进去，最后就变成了上面的情况，可以满足条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我们总可以找到一个比 $a_{[l,r]}$ 更优的答案，&lt;strong&gt;假设不成立&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以结论成立。&lt;/p&gt;
&lt;h4&gt;思路&lt;/h4&gt;
&lt;p&gt;先找到序列 $a$ 的众数 $x$，若存在多个众数，则答案为 $n$.&lt;/p&gt;
&lt;p&gt;对答案中&lt;strong&gt;众数的出现次数&lt;/strong&gt;进行根号分治，分别采用不同的方法处理。&lt;/p&gt;
&lt;p&gt;设 $\operatorname{C}$ 为&lt;strong&gt;答案子段的众数的出现次数&lt;/strong&gt;，$x,y$ 表示答案子段的众数，$\operatorname{cnt_i}$ 为值 $i$ 在序列 $a$ 中的出现次数。&lt;/p&gt;
&lt;p&gt;下面进行分类讨论。&lt;/p&gt;
&lt;h4&gt;$\operatorname{C} &gt; \sqrt{n}$ 的情况&lt;/h4&gt;
&lt;p&gt;由于答案子段一定包含众数 $x$，且 $\operatorname{C}&gt;\sqrt{n}$，所以 $y$ 也满足 $\operatorname{cnt}_y&gt;\sqrt{n}$.&lt;/p&gt;
&lt;p&gt;显然，满足这个条件的 $y$ 最多有 $\sqrt{n}$ 个，考虑枚举 $y$。&lt;/p&gt;
&lt;p&gt;只需在 $\mathcal{O}(n)$ 的复杂度内&lt;strong&gt;求出：以 $(x,y)$ 作为众数的最大子段长度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此时问题只与 $x,y$ 这两个值有关，需要用到一个常见套路，&lt;strong&gt;让 $x$ 的权值为 $1$，$y$ 的权值为 $-1$，其余为 $0$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;只需在新序列中找：数值和为 $0$ 的最长子段。&lt;/p&gt;
&lt;p&gt;对新数列维护前缀和 $s_i$，子段 $[l,r]$ 的数值和为 $0$ 等价于：$s_r=s_{l-1}$，只需记录每个 $s_i$ 的最早出现位置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 1. 答案区间中众数的出现次数 &gt; sqrt(n)，枚举所有可行的数值check
for(int i = 1; i &amp;#x3C;= n; i++) if(cnt[i] &gt;= Sqrt &amp;#x26;&amp;#x26; i != mxval) {
    memset(L, 0, sizeof(L)); L[0 + N] = 0;
    for(int j = 1; j &amp;#x3C;= n; j++) {
        s[j] = s[j - 1] + (a[j] == mxval ? 1 : (a[j] == i ? -1 : 0));
        if(!L[s[j] + N] &amp;#x26;&amp;#x26; s[j]) L[s[j] + N] = j; //注意这里下标可能为负，所以要都加上N
        else ans = max(ans, j - L[s[j] + N]);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 $s_i\in[-n, n]$，记录最早出现位置的数组，下标需要整体 $i\leftarrow i+n$.&lt;/p&gt;
&lt;h4&gt;$\operatorname{C} \leq \sqrt{n}$ 的情况&lt;/h4&gt;
&lt;p&gt;考虑枚举 $\operatorname{C}$，只需在 $\mathcal{O}(n)$ 的复杂度内&lt;strong&gt;求出：众数出现次数为 $\operatorname{C}$ 的最大子段&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 class=&quot;language-cpp&quot;&gt;// 2. 答案区间中众数的出现次数 &amp;#x3C;= sqrt(n)，枚举所有可行的答案，双指针判断是否合法
for(int C = 1; C &amp;#x3C;= Sqrt; C++) {
    memset(cnt, 0, sizeof(cnt));
    for(int l = 1, r = 1, C_cnt = 0; r &amp;#x3C;= n; r++) {
        cnt[a[r]]++; if(cnt[a[r]] == C) C_cnt++;
        while(l &amp;#x3C;= r &amp;#x26;&amp;#x26; cnt[a[r]] &gt; C) {
            if(cnt[a[l]] == C) C_cnt--;
            cnt[a[l++]]--;
        }
        if(C_cnt &gt;= 2) ans = max(ans, r - l + 1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，$\operatorname{C_{cnt}}$ 代表出现次数为 $\operatorname{C}$ 的数的个数，动态维护桶。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 2e5 + 10, N = 2e5;
int n, a[MAXN], ans;
int L[MAXN &amp;#x3C;&amp;#x3C; 1], s[MAXN]; //情况一，L[i]为前缀和i第一次出现的下标，s[i]为前缀和
int cnt[MAXN]; //情况2，对于目前的尺取区间，cnt是对a[]的桶
int main() {
	ios::sync_with_stdio(false);
	cin &gt;&gt; n;
	int Sqrt = (int)(sqrt(n));
	for(int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; a[i], cnt[a[i]]++;
	int mxcnt = 0, mxval = -1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		if(cnt[a[i]] &gt; mxcnt) mxcnt = cnt[a[i]], mxval = a[i];
		else if(cnt[a[i]] == mxcnt &amp;#x26;&amp;#x26; a[i] != mxval) mxval = n + 1;
	}
	if(mxval == n + 1) { cout &amp;#x3C;&amp;#x3C; n &amp;#x3C;&amp;#x3C; endl; return 0; } //若众数有俩，直接输出n
//	1. 答案区间中众数的出现次数 &gt; sqrt(n)，枚举所有可行的数值check
	for(int i = 1; i &amp;#x3C;= n; i++) if(cnt[i] &gt;= Sqrt &amp;#x26;&amp;#x26; i != mxval) {
		memset(L, 0, sizeof(L)); L[0 + N] = 0;
		for(int j = 1; j &amp;#x3C;= n; j++) {
			s[j] = s[j - 1] + (a[j] == mxval ? 1 : (a[j] == i ? -1 : 0));
			if(!L[s[j] + N] &amp;#x26;&amp;#x26; s[j]) L[s[j] + N] = j; //注意这里下标可能为负，所以要都加上N
			else ans = max(ans, j - L[s[j] + N]);
		}
	}
//	2. 答案区间中众数的出现次数 &amp;#x3C;= sqrt(n)，枚举所有可行的答案，双指针判断是否合法
	for(int C = 1; C &amp;#x3C;= Sqrt; C++) {
		memset(cnt, 0, sizeof(cnt));
		for(int l = 1, r = 1, C_cnt = 0; r &amp;#x3C;= n; r++) {
			cnt[a[r]]++; if(cnt[a[r]] == C) C_cnt++;
			while(l &amp;#x3C;= r &amp;#x26;&amp;#x26; cnt[a[r]] &gt; C) {
				if(cnt[a[l]] == C) C_cnt--;
				cnt[a[l++]]--;
			}
			if(C_cnt &gt;= 2) ans = max(ans, r - l + 1);
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>线性基</title><link>https://www.tonyyin0418.com/blog/oi-notes/linear-basis</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/linear-basis</guid><description>线性基笔记，还没写完</description><pubDate>Mon, 20 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线性基&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;通常可以解决有关异或的一些题目。&lt;/p&gt;
&lt;p&gt;对于给定的数集 $A$，线性基是一个子集 $P\subset A$，其满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原序列里面的任意一个数，都可以由线性基里面的若干个数按位异或得到；&lt;/li&gt;
&lt;li&gt;任意若干个数，异或和不为 $0$.&lt;/li&gt;
&lt;li&gt;是满足上述条件的最小子集。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能求出：给定集合的最大异或和。&lt;/p&gt;
&lt;h2&gt;构造&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//插入一个long long范围内的整数
int p[65];
void insert(int x) {
    for(int i = 62; i &gt;= 0; i--) {
        if(x &amp;#x26; (1ll &amp;#x3C;&amp;#x3C; i)) {
            if(!p[i]) { p[i] = x; break; }
            x ^= p[i];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在二进制下，$p[i]$ 代表线性基中，最高位是第 $i$ 位的元素。&lt;/p&gt;
&lt;p&gt;显然每个数位最多需要一个元素，因为当 $p_i=2^i$ 时，所有数都能被线性基表出。&lt;/p&gt;
&lt;p&gt;上面这段代码中，用 $x_i$ 表示 &lt;code&gt;x&lt;/code&gt; 在运行过程中的取值，那么其含义为：&lt;strong&gt;要让 $x$ 能被线性基表出，则只需 $x_i$ 可以被线性基表出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;循环第一次进行时，我们需要 $x$ 能被表出，用 $i$ 表示二进制下 $x$ 的最高位，则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 $p_i$ 为空，$p_i\leftarrow x$，直接填入后跳出循环。&lt;/li&gt;
&lt;li&gt;若 $p_i$ 非空，由于 $p_i$ 已经可以被表出了，要想让 $x$ 能被表出，就只需要 $x\oplus p_i$ 可以被表出。所以让 $x\leftarrow x\oplus p_i$，注意到 $p_i\oplus x$ 的最高位会变小，会在后面的循环中被处理，这很合理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若当循环结束时，$p_i\leftarrow x$ 还没有被执行过，说明 $x$ 在插入前就可以被线性基表出了。&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;/p&gt;
&lt;p&gt;代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Linear_Basis{
	int p[24], pos[24];
	void insert(int x, int id) {
		for(int i = 23; i &gt;= 0; i--) {
			if(x &amp;#x26; (1ll &amp;#x3C;&amp;#x3C; i)) {
				if(!p[i]) { p[i] = x, pos[i] = id; break; }
				else if(id &gt; pos[i]) { swap(pos[i], id), swap(x, p[i]); }
				x ^= p[i];
			}
		}
	}
	int qry_max(int l) {
		int ret = 0;
		for(int i = 23; i &gt;= 0; i--)
			if(pos[i] &gt;= l &amp;#x26;&amp;#x26; (ret ^ p[i]) &gt; ret) ret ^= p[i];
		return ret;
	}
} lb;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽可能用右侧的数来作为基，所以要用 $x$ 替换掉 $p_i$ 的位置。&lt;/p&gt;
&lt;p&gt;插入之后需要 $\mathrm{swap}(x, p_i)$，因为此时的线性基已经能表出 $x$ 了，但原先的 $p_i$ 不能被表示了。&lt;/p&gt;
&lt;p&gt;直接取出所有 $pos_i\geq l$ 的 $p_i$，就是 $a_{[l,r]}$ 的线性基。&lt;/p&gt;
&lt;p&gt;原因：还是要看上面插入函数流程的含义。若一个 $p_i$ 所对应的 $pos_i&amp;#x3C;l$，说明在插入 $a_{[l,r]}$ 这些数时，$p_i$ 是否存在于 $a_{[l,r]}$ 能否被表出是无关的。因此 $pos_i&amp;#x3C;l$ 的 $p_i$ 不在 $a_{[l,r]}$ 的线性基中。&lt;/p&gt;</content:encoded></item><item><title>2022省选集训 – 优化基础技巧</title><link>https://www.tonyyin0418.com/blog/oi-solution/bjoi2022-a2</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/bjoi2022-a2</guid><description>整除分块，01分数规划</description><pubDate>Wed, 15 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;整除分块&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;整除分块，用于快速求出形如
$$
\sum_{i=1}^{n}f(\left\lfloor\frac{n}{i} \right\rfloor)
$$
的式子。&lt;/p&gt;
&lt;p&gt;随着 $i$ 的变化，$\left\lfloor\frac{n}{i}\right\rfloor$ 大约只有 $\sqrt{n}$ 种不同的取值，并且取值相同的数是连续的。也就是一段数满足：
$$
\left\lfloor\frac{n}{l} \right\rfloor=\left\lfloor\frac{n}{l+1} \right\rfloor=\cdots=\left\lfloor\frac{n}{r-1} \right\rfloor=\left\lfloor\frac{n}{r} \right\rfloor
$$
我们想要&lt;strong&gt;求出这个区间的左右端点&lt;/strong&gt; $l, r$，这样 $f(\left\lfloor\dfrac{n}{i} \right\rfloor), i\in[l, r]$ 就只需要算一次，再乘上 $(r-l+1)$ 即可。&lt;/p&gt;
&lt;p&gt;注意到端点满足：$\left\lfloor\dfrac{n}{l} \right\rfloor=\left\lfloor\dfrac{n}{r} \right\rfloor=k\in \Bbb{Z}$，在此条件下，我们求 $r$ 的最大值，所以有关系：
$$
\begin{cases}
k=\left\lfloor\dfrac{n}{l} \right\rfloor\
r=\max(i),\quad i\times k\leq n
\end{cases}
$$
因此
$$
r=\left\lfloor\dfrac{n}{k} \right\rfloor=\left\lfloor\dfrac{n}{\left\lfloor\dfrac{n}{l} \right\rfloor} \right\rfloor
$$
于是我们知道了，&lt;strong&gt;对于任意 $i$ 满足 $l\leq i\leq \left\lfloor\dfrac{n}{\left\lfloor\frac{n}{l} \right\rfloor} \right\rfloor$，其 $\left\lfloor\dfrac{n}{i} \right\rfloor$ 的值都是相同的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/StarRoadTang/p/14751372.html&quot;&gt;参考资料&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/UVA11526&quot;&gt;UVA11526 H(n)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;求 $\sum_{i=1}^{n} \lfloor \frac{n}{i} \rfloor$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int l = 1, r; l &amp;#x3C;= n; l = r + 1) {
    r = n / (n / l);
    ans += (r - l + 1) * (n / l);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2261&quot;&gt;P2261 [CQOI2007]余数求和&lt;/a&gt;&lt;/h2&gt;
&lt;h4&gt;题意&lt;/h4&gt;
&lt;p&gt;给定 $n, k$，求：
$$
G(n, k)=\sum_{i=1}^n{k\bmod i}
$$&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{ans}&amp;#x26;=\sum_{i=1}^{n}k\bmod i\
&amp;#x26;=\sum_{i=1}^{n}k-i\times \left\lfloor\frac{k}{i}\right\rfloor\
&amp;#x26;=n\cdot k-\sum_{i=1}^{n}i\times \left\lfloor\frac{k}{i}\right\rfloor\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;减号后面用整除分块，于是在 $\mathcal{O}(\sqrt{k})$ 的时间复杂度内解决。&lt;/p&gt;
&lt;h1&gt;01分数规划&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;h4&gt;描述一&lt;/h4&gt;
&lt;p&gt;每种物品有两个权值 $a, b$，可以从中选出其中一些，求 $\dfrac{\sum{a_i}}{\sum{b_i}}$ 的极值。&lt;/p&gt;
&lt;h4&gt;描述二&lt;/h4&gt;
&lt;p&gt;给定数列 $a_i$，$b_i$，找一组 $w_i\in{0, 1}$ 使 $\frac{\sum w_i\times a_i}{\sum w_i\times b_i}$ 最大，只需求出这个最大值。&lt;/p&gt;
&lt;h4&gt;思路&lt;/h4&gt;
&lt;p&gt;二分答案，以求最大值为例。&lt;/p&gt;
&lt;p&gt;若 $\operatorname{mid}$ 可行，则存在一组 $w_i$ 满足：
$$
\begin{aligned}
&amp;#x26;\frac{\sum a_i\times w_i}{\sum b_i\times w_i} &gt; \operatorname{mid}\
\Rightarrow &amp;#x26;\sum a_i\times w_i-\operatorname{mid} \sum b_i\times w_i&gt;0\
\Rightarrow &amp;#x26;\sum w_i(a_i-\operatorname{mid}\times b_i)&gt;0
\end{aligned}
$$
应用见例题。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4377&quot;&gt;P4377 [USACO18OPEN]Talent Show G&lt;/a&gt;&lt;/h2&gt;
&lt;h4&gt;题意&lt;/h4&gt;
&lt;p&gt;给定数列 $a_i$，$b_i$，找一组 $w_i\in{0, 1}$ 使 $\frac{\sum w_i\times a_i}{\sum w_i\times b_i}$ 最大，求出这个&lt;strong&gt;最大值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同时要求 $\sum w_i\times b_i\geq W$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$W$ 是给定常数，$n\leq 250$，$W\leq 1000$.&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;需要在满足 $\sum w_i\times b_i\geq W$ 的条件下，求出 $[\sum w_i(a_i-\operatorname{mid}\times b_i)]_{\operatorname{max}}$.&lt;/p&gt;
&lt;p&gt;这可以用背包解决。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P4322&quot;&gt;P4322 [JSOI2016]最佳团体&lt;/a&gt;&lt;/h2&gt;
&lt;h4&gt;题意&lt;/h4&gt;
&lt;p&gt;给定一棵 $n$ 个节点的树，每个点有权值 $a_i, b_i$.&lt;/p&gt;
&lt;p&gt;选出恰好 $k$ 个点，且要求：&lt;strong&gt;若节点 $u$ 被选中，则 $\operatorname{Father}_u$ 也一定要被选&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;求 $\dfrac{\sum a_i}{\sum b_i}$ 的&lt;strong&gt;最大值&lt;/strong&gt;，$n,k\leq 2500$.&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;需要在满足题目条件的前提下，求出 $[\sum (a_i-\operatorname{mid}\times b_i)]_{\operatorname{max}}$.&lt;/p&gt;
&lt;p&gt;考虑树上背包，设 $f[u][j]$ 表示在 $u$ 的子树中，取出 $j$ 个节点的最大收益值。&lt;/p&gt;
&lt;p&gt;求出 $f$ 后，只需判断 $f[\operatorname{root}][k]&gt;0$ 是否成立。&lt;/p&gt;
&lt;p&gt;**转移方法：**首先有 $f[u][0]=0$。若 $j\neq 0$，则节点 $u$ 一定被取，分别从 $u$ 的每棵子树 $v$ 进行转移。即：
$$
f[u][j] =
\left{
\begin{array}{lr}
0 ,&amp;#x26; j=0\
\max\limits_{k&amp;#x3C;j}(f[u][j-k]+f[v][k]) ,&amp;#x26; j\neq 0
\end{array}
\right.
$$&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3705&quot;&gt;P3705 [SDOI2017]新生舞会&lt;/a&gt;&lt;/h2&gt;
&lt;h4&gt;题意&lt;/h4&gt;
&lt;p&gt;有两个点集 $A, B$，均包含 $n$ 个元素。现在要让不同点集之间的元素两两配对。&lt;/p&gt;
&lt;p&gt;对于每个配对 $(A_i, B_j)$，会产生两个权值 $a_{i, j}$ 和 $b_{i, j}$.&lt;/p&gt;
&lt;p&gt;找一组配对方案，使 $\dfrac{\sum a}{\sum b}$ 最大，求出这个最大值。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 100, 1\leq a_{i, j}, b_{i, j}\leq 10^4$.&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;先做相同的转化。这道题中，求 $[\sum w_i(a_i-\operatorname{mid}\times b_i)]_{\operatorname{max}}$ 的方法是&lt;strong&gt;最大费用最大流&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;建一个二分图，每条边流量均为 $1$，二分图之间连边的费用为对应的权值 $w=a_{i_j}-\operatorname{mid}\times b_{i, j}$.&lt;/p&gt;</content:encoded></item><item><title>计算几何</title><link>https://www.tonyyin0418.com/blog/oi-notes/geometry</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/geometry</guid><description>2021.12.07，计算几何专题分享，二维凸包、三维凸包、旋转卡壳、半平面交</description><pubDate>Tue, 07 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;向量&lt;/h1&gt;
&lt;h2&gt;平面向量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;向量：&lt;/strong&gt; 既有大小又有方向的量称为向量，记作 $\vec {a}$ 或 $\boldsymbol a$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有向线段：&lt;/strong&gt; 带方向的线段。用有向线段来直观地表示向量。起点为 $A$ 终点为 $B$ 的有向线段表示的向量，用符号简记为 $\overrightarrow{AB}$.&lt;/p&gt;
&lt;p&gt;**向量的模：**向量的大小（或长度），用 $|\overrightarrow{AB}|$ 或 $|\boldsymbol{a}|$ 表示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;零向量&lt;/strong&gt;：模为 $0$ 的向量。零向量的方向任意。记为：$\vec 0$ 或 $\boldsymbol{0}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单位向量&lt;/strong&gt;：模为 $1$ 的向量称为该方向上的单位向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;平行向量&lt;/strong&gt;：方向相同或相反的两个 &lt;strong&gt;非零&lt;/strong&gt; 向量。规定 $\boldsymbol 0$ 与任意向量平行。$\boldsymbol{a}$ 与 $\boldsymbol{b}$ 平行，记作：$\boldsymbol{a}\parallel \boldsymbol{b}$ 。&lt;/p&gt;
&lt;p&gt;**共线向量：**与平行向量的定义相同。任一组平行向量都可以平移到同一直线上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;向量的夹角&lt;/strong&gt;：已知两个非零向量 $\boldsymbol a,\boldsymbol b$，作 $\overrightarrow{OA}=\boldsymbol a,\overrightarrow{OB}=\boldsymbol b$，那么 $\theta=\angle AOB$ 就是向量 $\boldsymbol a$ 与向量 $\boldsymbol b$ 的夹角。记作：$\langle \boldsymbol a,\boldsymbol b\rangle$。当 $\theta=\frac{\pi}{2}$ 时，称这两个向量垂直，记作 $\boldsymbol a\perp \boldsymbol b$。规定 $\theta \in [0,\pi]$。&lt;/p&gt;
&lt;h2&gt;向量的线性运算&lt;/h2&gt;
&lt;h3&gt;向量的加法&lt;/h3&gt;
&lt;h4&gt;向量加法的三角形法则&lt;/h4&gt;
&lt;p&gt;对于平面上的任意两个向量 $\boldsymbol{a}$ 和 $\boldsymbol{b}$，在平面内任取一点 $A$，作 $\overrightarrow{AB}=\boldsymbol{a}$，$\overrightarrow{BC}=\boldsymbol{b}$，作向量 $\overrightarrow{AC}$.&lt;/p&gt;
&lt;p&gt;称向量 $\overrightarrow{AC}$ 为向量 $\boldsymbol{a}$ 和 $\boldsymbol{b}$ 的 &lt;strong&gt;和向量&lt;/strong&gt;，$\overrightarrow{AB}+\overrightarrow{BC}=\overrightarrow{AC}.$&lt;/p&gt;
&lt;p&gt;如图1，把向量首尾顺次相连，向量的和为第一个向量的起点指向最后一个向量的终点；&lt;/p&gt;
&lt;h4&gt;向量加法的平行四边形法则&lt;/h4&gt;
&lt;p&gt;若要求和的两个向量 &lt;strong&gt;共起点&lt;/strong&gt;，那么它们的和向量为以这两个向量为邻边的平行四边形的对角线，起点为两个向量共有的起点，方向沿平行四边形对角线方向。&lt;/p&gt;
&lt;h3&gt;向量的减法&lt;/h3&gt;
&lt;p&gt;减法可以写成加上相反数的形式，即：$a-b=a+(-b)$，如图1，$b=c-a$，$a=c-b$.&lt;/p&gt;
&lt;h3&gt;向量的数乘&lt;/h3&gt;
&lt;p&gt;给定一个实数 $\lambda$ 和一个向量 $\boldsymbol a$，规定其乘积为一个向量，记作 $\lambda \boldsymbol a$，其模与方向定义如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$|\lambda \boldsymbol a|=|\lambda| |\boldsymbol a|$；&lt;/li&gt;
&lt;li&gt;当 $\lambda &gt;0$ 时，$\lambda\boldsymbol a$ 与 $\boldsymbol a$ 同向，当 $\lambda =0$ 时，$\lambda \boldsymbol a=\boldsymbol 0$，当 $\lambda&amp;#x3C;0$ 时，$\lambda \boldsymbol a$ 与 $\boldsymbol a$ 方向相反。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种运算是向量的数乘运算。&lt;/p&gt;
&lt;h3&gt;坐标表示&lt;/h3&gt;
&lt;p&gt;$$
\begin{aligned}
\boldsymbol a + \boldsymbol b=(\boldsymbol a_x+\boldsymbol b_x,;\boldsymbol a_y+\boldsymbol b_y)\
\boldsymbol a - \boldsymbol b=(\boldsymbol a_x-\boldsymbol b_x,;\boldsymbol a_y-\boldsymbol b_y)\
\lambda \boldsymbol a=(\lambda\boldsymbol a_x,;\lambda\boldsymbol a_y)
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;向量的数量积&lt;/h2&gt;
&lt;p&gt;已知两个向量 $\boldsymbol a,\boldsymbol b$，它们的夹角为 $\theta$，那么：
$$
\boldsymbol a \cdot \boldsymbol b=|\boldsymbol a||\boldsymbol b|\cos \theta=\boldsymbol a_x\boldsymbol b_x+\boldsymbol a_y\boldsymbol b_y
$$
就是这两个向量的 &lt;strong&gt;数量积&lt;/strong&gt;，也叫 &lt;strong&gt;点积&lt;/strong&gt; 或 &lt;strong&gt;内积&lt;/strong&gt;。其中称 $|\boldsymbol a|\cos \theta$ 为 $\boldsymbol a$ 在 $\boldsymbol b$ 方向上的投影。数量积的几何意义即为：数量积 $\boldsymbol a \cdot \boldsymbol b$ 等于 $\boldsymbol a$ 的模与 $\boldsymbol b$ 在 $\boldsymbol a$ 方向上的投影的乘积。&lt;/p&gt;
&lt;p&gt;这种运算得到的结果是一个实数，为标量。&lt;/p&gt;
&lt;p&gt;可以方便的计算出 $\cos \theta$，于是有如下应用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;$\boldsymbol a \perp \boldsymbol b$ $\iff$ $\boldsymbol a\cdot \boldsymbol b=0$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\boldsymbol a = \lambda \boldsymbol b$ $\iff$ $|\boldsymbol a\cdot \boldsymbol b|=|\boldsymbol a||\boldsymbol b|$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$|\boldsymbol a|=\sqrt {m^2+n^2}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\cos \theta=\cfrac{\boldsymbol a\cdot\boldsymbol b}{|\boldsymbol a||\boldsymbol b|}$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;向量的向量积&lt;/h2&gt;
&lt;p&gt;给定两个向量 $\boldsymbol a,\boldsymbol b$，规定其向量积为一个向量，记作 $\boldsymbol a\times \boldsymbol b$，其模与方向定义如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;$|\boldsymbol a\times \boldsymbol b|=|\boldsymbol a||\boldsymbol b|\sin \langle \boldsymbol a,\boldsymbol b\rangle$；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\boldsymbol a\times \boldsymbol b$ 与 $\boldsymbol a,\boldsymbol b$ 都垂直，且 $\boldsymbol a,\boldsymbol b,\boldsymbol a\times \boldsymbol b$ 符合右手法则。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;向量积也叫外积，其几何意义是：&lt;strong&gt;$|\boldsymbol a\times \boldsymbol b|$ 是以 $\boldsymbol a,\boldsymbol b$ 为邻边的平行四边形的面积&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;向量积与 $\boldsymbol a,\boldsymbol b$ 所在平面垂直，其竖坐标为 $\boldsymbol a_x\boldsymbol b_y-\boldsymbol a_y\boldsymbol b_x$.&lt;/p&gt;
&lt;p&gt;我们根据右手法则可以推断出 $\boldsymbol b$ 相对于 $\boldsymbol a$ 的方向，&lt;strong&gt;逆时针方向竖坐标为正值，反之为负值&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;坐标旋转公式&lt;/h2&gt;
&lt;p&gt;若将向量 $\boldsymbol a=(x,;y)$ 逆时针旋转 $\alpha$，得到向量 $\boldsymbol b$，则有：
$$
\boldsymbol b=(x\cos \alpha-y\sin \alpha,;y\cos \alpha+x\sin\alpha)
$$
可以通过三角恒等变换证明。&lt;/p&gt;
&lt;h2&gt;参考代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const double pi = 3.1415926535898;
const double eps = 1e-8;
inline int sgn(double x) {
	if(fabs(x) &amp;#x3C; eps) return 0;
	else return x &gt; 0 ? 1 : -1;
}
inline int fcmp(double x, double y) {
	if(fabs(x - y) &amp;#x3C; eps) return 0;
	else return x &gt; y ? 1 : -1;
}
struct Point{
	double x, y;
	Point(){};
	Point(double a, double b): x(a), y(b) {}
	Point(Point a, Point b): x(b.x - a.x), y(b.y - a.y) {}
	friend Point operator + (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x + b.x, a.y + b.y);
	}
	friend Point operator - (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a.x - b.x, a.y - b.y);
	}
	friend bool operator == (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return fcmp(a.x, b.x) == 0 &amp;#x26;&amp;#x26; fcmp(a.y, b.y == 0);
	}
	friend Point operator * (const double &amp;#x26;a, const Point &amp;#x26;b) {
		return Point(a * b.x, a * b.y);
	}
	friend Point operator * (const Point &amp;#x26;a, const double &amp;#x26;b) {
		return Point(a.x * b, a.y * b);
	}
	friend double operator * (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x * b.y - a.y * b.x;
	}
	friend double operator &amp;#x26; (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return a.x * b.x + a.y * b.y;
	}
	friend Point operator / (const Point &amp;#x26;a, const double &amp;#x26;b) {
		return Point(a.x / b, a.y / b);
	}
	friend bool operator &amp;#x3C; (const Point &amp;#x26;a, const Point &amp;#x26;b) {
		return (a.x &amp;#x3C; b.x) || (a.x == b.x &amp;#x26;&amp;#x26; a.y &amp;#x3C; b.y);
	}
	inline double len() {
		return sqrt(1.0 * x * x + 1.0 * y * y);
	}
	friend double area(Point &amp;#x26;a, Point &amp;#x26;b, Point &amp;#x26;c) {
		return (b - a) * (c - a) / 2.0;
	}
};
typedef Point Vec;
inline double dis(Point &amp;#x26;a, Point &amp;#x26;b) {
	return (a - b).len();
}
inline double angle(Point &amp;#x26;a, Point &amp;#x26;b) {
	return acos((a &amp;#x26; b) / a.len() / b.len());
}
inline Vec rotate(Vec &amp;#x26;a, double k) {
	return Vec(a.x * cos(k) - a.y * sin(k), a.x * sin(k) - a.y * cos(k));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;平面凸包&lt;/h1&gt;
&lt;h2&gt;相关概念&lt;/h2&gt;
&lt;p&gt;**凸多边形：**所有内角大小都在 $[0,\pi]$ 范围内的 &lt;strong&gt;简单多边形&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;**平面凸包：**平面上的一个子集 $S$ 被称为凸的，当且仅当对于任意两点 $p, q\in S$，线段 $\overline{pq}$ 都完全属于 $S$.集合 $S$ 的凸包 $\mathcal{CH}(S)$，是包含 $S$ 的最小凸集，也就是包含 $S$ 的所有凸集的交。&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;/p&gt;
&lt;h2&gt;暴力求解&lt;/h2&gt;
&lt;p&gt;用二维坐标 $(x_i, y_i)$ 的形式给定点集 $P$，考虑如何暴力求解。&lt;/p&gt;
&lt;p&gt;注意到，若线段 $\overline{pq}$ 在凸包上，则 $P$ 中的点均位于直线 $pq$ 的同一侧。若我们钦定 $p\rightarrow q$ 按顺时针方向，则有更强的限制，需要 $P$ 中的点都在直线的右侧。&lt;/p&gt;
&lt;p&gt;于是可以枚举有序点对 $(p, q)\in P\times P$，若 $P$ 中的点都在有向线段 $\overline{pq}$ 的右侧，则 $\overline{pq}$ 是 $\mathcal{CH}(P)$ 中的一条边。&lt;/p&gt;
&lt;p&gt;需要用到向量的叉积，点 $t$ 在 $\overrightarrow {pq}$ 右侧 $\Longleftrightarrow$ $\overrightarrow{pt}\times \overrightarrow{pq}&gt;0$.&lt;/p&gt;
&lt;p&gt;这样的复杂度是 $\mathcal{O}(n^3)$ 的，有很多可以优化的地方。&lt;/p&gt;
&lt;h2&gt;Andrew算法&lt;/h2&gt;
&lt;p&gt;Andrew 算法是一种递增式算法，流程如下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;递增式算法 (incremental algorithm)，在计算几何中常见。算法思想：逐一加入 $P$ 中的点，每增加一个点，都更新一次目前的解，加入最后一个点后，即可得到答案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&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;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;设当前枚举到点 $P$，即将把其加入凸包；当前栈顶的点为 $S_1$，栈中第二个点为 $S_2$.&lt;/p&gt;
&lt;p&gt;求凸包时，若 $P$ 与 $S_1$ 构成的新线段是顺时针旋转的，即叉积满足：$\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}&amp;#x3C;0$，则弹出栈顶，继续检查，直到 $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}\ge 0$ 或者栈内仅剩一个元素为止。&lt;/p&gt;
&lt;p&gt;上图是一个弹栈的例子，$p_i$ 是新加入的点，细线是加入 $p_i$ 之前的凸包状态。&lt;/p&gt;
&lt;p&gt;记 $n=|P|$，则时间复杂度为 $\mathcal{O}(n\log n)$，瓶颈在排序部分。&lt;/p&gt;
&lt;p&gt;如上图，若将弹出栈顶元素的条件改为 $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}\leq 0$，同时停止条件改为 $\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}&gt; 0$，则求出的凸包中不存在三点共线。可视情况更改。&lt;/p&gt;
&lt;p&gt;下面是参考代码。函数返回值为凸包的点数，&lt;code&gt;Point ret[] &lt;/code&gt;的下标从 $0$ 开始。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;inline bool check(Point s1, Point s2, Point p) {
	return Vec(s2, s1) * Vec(s1, p) &gt; 0;
}
int Convex_hull_2d(int n, Point *p, Point *ret) {
	sort(p, p + n, cmp1);
	int top = -1;
	for (int i = 0; i &amp;#x3C; n; i++) {
		while (top &gt; 0 &amp;#x26;&amp;#x26; !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	int k = top;
	for (int i = n - 2; i &gt;= 0; i--) {
		while (top &gt; k &amp;#x26;&amp;#x26; !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	return top;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Graham算法&lt;/h2&gt;
&lt;p&gt;Andrew 算法是 Graham 算法的改进版本。在 Graham 算法中，点集按照极角序排序。&lt;/p&gt;
&lt;p&gt;**极角：**任取一个顶点 $O$ 作为极点，作射线 $OX$，称为极轴。平面上一点 $p$ 的极角，即为向量 $\overrightarrow{Op}$ 与极轴 $OX$ 的夹角。一般地，取 $x$ 轴作为极轴，以逆时针方向为正。&lt;/p&gt;
&lt;p&gt;可以利用 &lt;code&gt;atan2(double y, double x)&lt;/code&gt; 进行极角排序。函数返回值为 $(x, y)$ 与 $x$ 轴的极角，数值 $\in(-\pi, \pi]$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool cmp(Point a, Point b) {
    if(atan2(a.y, a.x) - atan2(b.y, b.x) == 0)
        return a.x &amp;#x3C; b.x;
    return atan2(a.y, a.x) &amp;#x3C; atan2(b.y, b.x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一种方式是利用叉积排序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool cmp(Point a, Point b) {
    return a * b &gt; 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意极角排序时，无论用 &lt;code&gt;atan2&lt;/code&gt; 还是叉积，精度上都会出现不少问题，尽量避免使用这种方法。&lt;/p&gt;
&lt;h2&gt;平面凸包的周长与面积&lt;/h2&gt;
&lt;p&gt;先求出&lt;strong&gt;按照顺时针排序&lt;/strong&gt;的，构成凸包的点集 $p$，记 $n=|p|$.&lt;/p&gt;
&lt;p&gt;**求周长：**把相邻两点组成的向量的模长求和，即：
$$
l=\sum_{i=1}^n|\overrightarrow{p_{i}p_{i+1}}|+|\overrightarrow{p_{1}p_{n}}|
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double dis(Point a, Point b) {
	return (a - b).len();
}
double Convex_hull_2d_L(int n, Point *p) {
	Point convex[N];
	int siz = Convex_hull_2d(n, p, convex);
	double ans = dis(convex[0], convex[siz - 1]);
	for (int i = 1; i &amp;#x3C; siz; i++)
		ans += dis(convex[i - 1], convex[i]);
	return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**求面积：**任取凸包内一点（一般取 $p_1$），则有：
$$
s=\sum_{i=2}^{n-1} \operatorname{area}(p_1, p_i, p_i+1)=\sum_{i=2}^{n-1}\frac{|(p_i-p_1)\times (p_{i+1}-p_1)|}{2}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double area(Point a, Point b, Point c) {
	return (b - a) * (c - a) / 2.0;
}
double Convex_hull_2d_S(int n, Point *p) {
	Point convex[N];
	int siz = Convex_hull_2d(n, p, convex);
	double ans = 0;
	for (int i = 2; i &amp;#x3C; siz; i++)
		ans += area(convex[0], convex[i - 1], convex[i]);
	return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;动态凸包（CF70D）&lt;/h1&gt;
&lt;p&gt;维护一个点集 $S$ 的凸包，需要支持如下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;询问点 $p$ 是否在当前的凸包中，&lt;/li&gt;
&lt;li&gt;向 $S$ 中添加点 $p$，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保证坐标均为整数。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;和 Andrew 算法一样，这里的算法按照坐标字典序排序。相对于极角排序，能够减小精度误差。&lt;/p&gt;
&lt;p&gt;用两个 &lt;code&gt;std::map&amp;#x3C;int, int&gt;&lt;/code&gt;，用 $\operatorname{top}$ 记录上凸包，$\operatorname{down}$ 记录下凸包。&lt;/p&gt;
&lt;p&gt;**存储方法：**若上凸包中存在横坐标为 $x$ 的点，则这个点的纵坐标为 $\operatorname{top}[x]$，$\operatorname{down}$ 同理。&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;以上凸包为例。$i$ 在上凸包之下，当且仅当 $|\overrightarrow{ik}\times \overrightarrow{ij}|\geq 0$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool check_top(int x, int y) { //是否在上凸包下面
	auto k = top.lower_bound(x);
	if(k == top.end())
		return false;
	if(k -&gt; first == x)
		return y &amp;#x3C;= k-&gt;second;
	if(k == top.begin()) return false;
	auto j = k; j--;
	return Point(k-&gt;first - x, k-&gt;second - y) *
		   Point(j-&gt;first - x, j-&gt;second - y) &gt;= 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下凸包同理，$i$ 在下凸包之上，当且仅当 $|\overrightarrow{ik}\times \overrightarrow{ij}|\leq 0$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool check_down(int x, int y) { //是否在下凸包上面
	auto k = down.lower_bound(x);
	if(k == down.end())
		return false;
	if(k -&gt; first == x)
		return y &gt;= k-&gt;second;
	if(k == down.begin()) return false;
	auto j = k; j--;
	return Point(k-&gt;first - x, k-&gt;second - y) *
		   Point(j-&gt;first - x, j-&gt;second - y) &amp;#x3C;= 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;插入操作&lt;/h2&gt;
&lt;p&gt;把 $p$ 点加入凸包，上下凸包都要尝试。把加入 $p$ 点后，删掉不满足凸性的点。&lt;/p&gt;
&lt;p&gt;如上图，这些点一定是分布在 $p_x$ 左右的连续段。&lt;/p&gt;
&lt;p&gt;因此找到 $p$ 点在上/下凸壳中的位置，向左右分别删点，直到满足凸性。&lt;/p&gt;
&lt;p&gt;注意迭代器的边界问题。如果已经删没了，要及时退出循环，否则会 RE。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void insert_top(int x, int y) {
	if(check_top(x, y)) return;
	top[x] = y;
	auto it = top.find(x);
	auto jt = it;
	if(it != top.begin()) { //remove left
		jt--;
		while(remove_top(jt++)) jt--;
	}
	if(++jt != top.end()) { //remove right
		while(remove_top(jt--)) jt++;
	}
}
void insert_down(int x, int y) {
	if(check_down(x, y)) return;
	down[x] = y;
	auto it = down.find(x);
	auto jt = it;
	if(it != down.begin()) { //remove left
		jt--;
		while(remove_down(jt++)) jt--;
	}
	if(++jt != down.end()) { //remove right
		while(remove_down(jt--)) jt++;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的函数用于：判断能否删除当前点，若能删，则执行删除操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool remove_top(map&amp;#x3C;int, int&gt;::iterator it) {
	if(it == top.begin()) return false; //到边界就不删了
	if(++it == top.end()) return false; it--;
	auto jt = it, kt = it;
	jt--; kt++;
	if(Point(it -&gt; first - jt -&gt; first, it-&gt;second - jt-&gt;second) *
	   Point(it -&gt; first - kt -&gt; first, it-&gt;second - kt-&gt;second) &amp;#x3C;= 0) {
		   top.erase(it);
		   return true;
	}
	return false;
}
bool remove_down(map&amp;#x3C;int, int&gt;::iterator it) {
	if(it == down.begin()) return false;
	if(++it == down.end()) return false; it--;
	auto jt = it, kt = it;
	--jt; ++kt;
	if(Point(it -&gt; first - jt -&gt; first, it-&gt;second - jt-&gt;second) *
	   Point(it -&gt; first - kt -&gt; first, it-&gt;second - kt-&gt;second) &gt;= 0) {
		   down.erase(it);
		   return true;
	}
	return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;三维凸包&lt;/h1&gt;
&lt;h2&gt;三维向量类&lt;/h2&gt;
&lt;p&gt;**存储：**用结构体记录三维坐标，方便重载运算符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Point3 {
	double x, y, z;
	Point3(){};
	Point3(double a, double b, double c) : x(a), y(b), z(c) {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;加法、减法和点乘等操作&lt;/strong&gt;：与二维向量类似。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Point3 operator + (const Point3 &amp;#x26;b) {
    return Point3(x + b.x, y + b.y, z + b.z);
}
Point3 operator - (const Point3 &amp;#x26;b) {
    return Point3(x - b.x, y - b.y, z - b.z);
}
Point3 operator * (const Point3 &amp;#x26;b) {
    return Point3(y*b.z - z*b.y, z*b.x - x*b.z, x*b.y - y*b.x);
}
bool operator == (const Point3 &amp;#x26;b) {
    return fcmp(x, b.x) == 0 &amp;#x26;&amp;#x26; fcmp(y, b.y) == 0 &amp;#x26;&amp;#x26; fcmp(z, b.z) == 0;
}
double len() {
    return sqrt(x * x + y * y + z * z);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;叉乘：&lt;/strong&gt;$\boldsymbol a\times \boldsymbol b$ 的结果为一个三维向量 $\boldsymbol c$，$\boldsymbol c\perp \boldsymbol a$ 且 $\boldsymbol c\perp \boldsymbol b$，结果向量的模长为 $|\boldsymbol a||\boldsymbol b|\sin \langle \boldsymbol a,\boldsymbol b\rangle$，代表以 $\boldsymbol a$、$\boldsymbol b$ 为两边的平行四边形的面积。&lt;/p&gt;
&lt;p&gt;在三维向量体系中，我们需要用坐标表示结果向量 $\boldsymbol{a}$，推导过程如下。（来源：&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%8F%89%E7%A7%AF&quot;&gt;叉积 - 维基百科&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;右手坐标系中，基向量 $\boldsymbol{i},\boldsymbol{j},\boldsymbol{k}$ 满足以下等式：
$$
\begin{array}{c}
\boldsymbol{i}\times\boldsymbol{j} = \boldsymbol{k}\
\boldsymbol{j\times i} = -\boldsymbol{k}\
\boldsymbol{j}\times\boldsymbol{k} = \boldsymbol{i}\
\boldsymbol{k\times j} = -\boldsymbol{i}\
\boldsymbol{k}\times\boldsymbol{i} = \boldsymbol{j}\
\boldsymbol{i\times k} = -\boldsymbol{j}
\end{array}
$$
根据外积的定义可以得出：$\boldsymbol{i}\times\boldsymbol{i} = \boldsymbol{j}\times\boldsymbol{j} = \boldsymbol{k}\times\boldsymbol{k} = \boldsymbol{0}$.&lt;/p&gt;
&lt;p&gt;根据以上等式，结合外积的&lt;strong&gt;分配律&lt;/strong&gt;，就可以确定任意向量的外积。&lt;/p&gt;
&lt;p&gt;任取向量 $\boldsymbol{u}=u_1\boldsymbol{i} + u_2\boldsymbol{j} + u_3\boldsymbol{k} $ 和 $\boldsymbol{v}=v_1\boldsymbol{i} + v_2\boldsymbol{j} + v_3\boldsymbol{k}$，两者的外积 $\boldsymbol{u} \times \boldsymbol{v}$ 可以根据分配率展开：
$$
\begin{align}
\boldsymbol{u}\times\boldsymbol{v} = &amp;#x26;(u_1\boldsymbol{i} + u_2\boldsymbol{j} + u_3\boldsymbol{k}) \times (v_1\boldsymbol{i} + v_2\boldsymbol{j} + v_3\boldsymbol{k})\
= &amp;#x26;u_1v_1(\boldsymbol{i} \times \boldsymbol{i}) + u_1v_2(\boldsymbol{i} \times \boldsymbol{j}) + u_1v_3(\boldsymbol{i} \times \boldsymbol{k}) + \
&amp;#x26;u_2v_1(\boldsymbol{j} \times \boldsymbol{i}) + u_2v_2(\boldsymbol{j} \times \boldsymbol{j}) + u_2v_3(\boldsymbol{j} \times \boldsymbol{k}) + \
&amp;#x26;u_3v_1(\boldsymbol{k} \times \boldsymbol{i}) + u_3v_2(\boldsymbol{k} \times \boldsymbol{j}) + u_3v_3(\boldsymbol{k} \times \boldsymbol{k})\
\end{align}
$$
把前面的 $6$ 个等式代入，则有：
$$
\begin{align}
\boldsymbol{u}\times\boldsymbol{v} = {} &amp;#x26;- u_1v_1\boldsymbol{0} + u_1v_2\boldsymbol{k} - u_1v_3\boldsymbol{j} \
&amp;#x26;- u_2v_1\boldsymbol{k} - u_2v_2\boldsymbol{0} + u_2v_3\boldsymbol{i} \
&amp;#x26;+ u_3v_1\boldsymbol{j} - u_3v_2\boldsymbol{i} - u_3v_3\boldsymbol{0} \
= {} &amp;#x26;(u_2v_3 - u_3v_2)\boldsymbol{i} + (u_3v_1 - u_1v_3)\boldsymbol{j} + (u_1v_2 - u_2v_1)\boldsymbol{k}\
\end{align}
$$
因此结果向量 $\boldsymbol{s} = \boldsymbol{u} \times \boldsymbol{v} = s_1\boldsymbol{i} + s_2\boldsymbol{j} + s_3\boldsymbol{k}$ 的三维坐标为：
$$
\begin{align}
s_1 &amp;#x26;= u_2v_3-u_3v_2\
s_2 &amp;#x26;= u_3v_1-u_1v_3\
s_3 &amp;#x26;= u_1v_2-u_2v_1
\end{align}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Point3 operator * (const Point3 &amp;#x26;b) {
    return Point3(y*b.z - z*b.y, z*b.x - x*b.z, x*b.y - y*b.x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;平面类&lt;/h2&gt;
&lt;p&gt;用三个向量表示一个三角形的平面。一个多面体可以通过三角剖分，用若干个三角形表示。&lt;/p&gt;
&lt;p&gt;为了节省空间，用 &lt;code&gt;point3 p[N]&lt;/code&gt; 存储所有可能出现的向量，结构体&lt;code&gt;plane&lt;/code&gt;只记录向量在 &lt;code&gt;p[]&lt;/code&gt; 中的下标。&lt;/p&gt;
&lt;p&gt;记录的三个向量按逆时针首尾相接，这样在判断方向时比较方便。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct plane{
	int v[3]; //逆时针
	plane(){};
	plane(int a, int b, int c) { v[0] = a, v[1] = b, v[2] = c; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**平面的法向量：**是指垂直于该平面的三维向量。一个平面具有无限个法向量，这些法向量有两个方向。&lt;/p&gt;
&lt;p&gt;根据叉积的性质，将三角形的两条邻边叉乘，得到的向量即为法向量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Point3 normal() {
    return (p[v[1]] - p[v[0]]) * (p[v[2]] - p[v[0]]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用法向量的模长，也可以算出三角形的面积。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double area() {
    return normal().len() / 2.0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三维凸包及性质&lt;/h2&gt;
&lt;p&gt;由 $n$ 个点构成的凸多面体。&lt;/p&gt;
&lt;p&gt;**性质：**根据欧拉公式，任意包含 $n$ 个顶点的凸多面体，所含的边不会超过 $3n-6$ 条，所含的小平面不会超过 $2n-4$ 张。&lt;/p&gt;
&lt;h2&gt;随机增量法&lt;/h2&gt;
&lt;h3&gt;算法思想&lt;/h3&gt;
&lt;p&gt;和 Andrew 算法类似，考虑每次把 $p_r$ 加入到前 $r-1$ 个点的凸包中，也就是将 $\mathcal{CH}(P_{r-1})$ 转化为 $\mathcal{CH}(P_{r})$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一种情况：&lt;/strong&gt;$p_r$ 在 $\mathcal{CH}(P_{r-1})$ 的内部或边界上，则 $\mathcal{CH}(P_{r-1})\rightarrow \mathcal{CH}(P_{r})$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二种情况：&lt;/strong&gt;$p_r$ 在 $\mathcal{CH}(P_{r-1})$ 外部。设想你站在 $p_r$ 所在的位置，看向 $\mathcal{CH}(P_{r-1})$. $\mathcal{CH}(P_{r-1})$ 中的某些小平面会被看到，其余在背面的平面不会被看到。如下图，从 $p_r$ 可见的平面构成了一片连通的区域。&lt;/p&gt;
&lt;p&gt;这片区域由一条封闭折线围成，称这条线为 $p_r$ 在 $\mathcal{CH}(P_{r-1})$ 上的&lt;strong&gt;边界线&lt;/strong&gt; $(\rm{horizon})$。&lt;/p&gt;
&lt;p&gt;根据这条地平线，我们可以判断出，在原先 $\mathcal{CH}(P_{r-1})$ 表面上的哪些部分需要被保留，哪些需要被替换。&lt;/p&gt;
&lt;p&gt;显然，不可见的平面在 $\mathcal{CH}(P_{r})$ 中被保留，并且我们用 $p_r$ 与地平线之间连接出新的小平面，来替换所有可见的小平面，如下图。&lt;/p&gt;
&lt;h3&gt;判断平面对点的可见性&lt;/h3&gt;
&lt;p&gt;如何用几何语言表达：一个平面对 $p_r$ 是可见的？&lt;/p&gt;
&lt;p&gt;对于一个凸包上的小平面，它能将空间分为两半，一侧是凸包外部，一侧是凸包内部。容易发现，如果点 $p$ 位于小平面的外侧，那么这个平面对于 $p$ 点就是可见的，因为凸包上的其它平面都不会有遮挡。&lt;/p&gt;
&lt;p&gt;形式化地，记 $a,b,c$ 为平面三角形的三个顶点，从凸包外部看，三点按照逆时针排列。&lt;/p&gt;
&lt;p&gt;利用叉乘的性质，记 $\boldsymbol{s}=\overrightarrow{ab}\times \overrightarrow{ac}$，则结果向量 $\boldsymbol{s}$ 是一个平面的法向量，且指向凸包外部。&lt;/p&gt;
&lt;p&gt;对于空间内任意一点 $p$，若 $\overrightarrow{ap}\times \boldsymbol{s}&gt;0$，则这个平面对点 $p$ 是可见的。&lt;/p&gt;
&lt;p&gt;下面是定义在结构体 &lt;code&gt;plane&lt;/code&gt; 中的函数，用于判断点 $A$ 是否位于平面的外侧。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool is_above(Point3 A) {
    return (normal() &amp;#x26; (A - p[v[0]])) &gt;= 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;求出边界线&lt;/h3&gt;
&lt;p&gt;要想把凸包从 $\mathcal{CH}(P_{r-1})$ 转化为 $\mathcal{CH}(P_{r})$，我们需要准确地求出凸包上的哪些边在边界线上。求出边界线之后，才能用 $p$ 与边界线构成的小平面替换需要被删掉的小平面。&lt;/p&gt;
&lt;p&gt;定义 &lt;code&gt;bool g[N][N]&lt;/code&gt;，$g[i][j]$ 表示 $\overrightarrow{p_ip_j}$ 所在的平面是否可见。&lt;/p&gt;
&lt;p&gt;若规定平面 $(a,b,c)$ 只包含 $\overrightarrow{ab},\overrightarrow{bc},\overrightarrow{ca}$，则对于任意有序数对 $(i, j)$，向量 $\overrightarrow{p_ip_j}$ 最多被包含在一个平面内。&lt;/p&gt;
&lt;p&gt;注意到，位于边界线上的向量 $\overrightarrow{p_ip_j}$ 一定满足 $g[i][j]=1$ 且 $g[j][i]=0$。所以我们只需对每个平面判断其可见性，并更新在 &lt;code&gt;g[][]&lt;/code&gt; 中对应的数值，即可求出边界线。&lt;/p&gt;
&lt;h3&gt;利用边界线更新凸包&lt;/h3&gt;
&lt;p&gt;在上一步遍历小平面时，若遇到的小平面是不可见的，则把它加入新的凸包中；若可见，则单独记录。&lt;/p&gt;
&lt;p&gt;之后遍历所有可见的小平面，若 $\overrightarrow{p_ip_j}$ 在边界线上，则把 $(p_i, p_j, p_r)$ 加入凸包中。$p_r$ 是新加入凸包的点。这样加入后的三点也满足逆时针排列。&lt;/p&gt;
&lt;h3&gt;参考代码&lt;/h3&gt;
&lt;p&gt;函数返回值为三维凸包的平面数，&lt;code&gt;plane ret[] &lt;/code&gt;的下标从 $0$ 开始。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int Convex_hull_3d(int n, plane *ret) {
	plane tmp[N];
	bool g[N][N];
	for (int i = 0; i &amp;#x3C; n; i++) p[i].shake();
	int top = -1;
	ret[++top] = plane(0, 1, 2);
	ret[++top] = plane(0, 2, 1);
	for (int i = 3; i &amp;#x3C; n; i++) {
		int cnt = -1;
		for (int j = 0; j &amp;#x3C;= top; j++) {
			bool flag = ret[j].is_above(p[i]);
			if (!flag)
				tmp[++cnt] = ret[j];
			for (int k = 0; k &amp;#x3C; 3; k++)
				g[ret[j].v[k]][ret[j].v[(k + 1) % 3]] = flag;
		}
		for (int j = 0; j &amp;#x3C;= top; j++) {
			for (int k = 0; k &amp;#x3C; 3; k++) {
				int a = ret[j].v[k], b = ret[j].v[(k + 1) % 3];
				if (g[a][b] &amp;#x26;&amp;#x26; !g[b][a])
					tmp[++cnt] = plane(a, b, i);
			}
		}
		for (int j = 0; j &amp;#x3C;= cnt; j++) ret[j] = tmp[j];
		top = cnt;
	}
	return (top + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;旋转卡壳&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;旋转卡壳算法用于：在线性时间内，求凸包直径、最小矩形覆盖等于凸包性质相关的问题。线性时间是指求出凸包之后的算法时间复杂度。&lt;/p&gt;
&lt;h2&gt;求凸包直径&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定平面上的 $n$ 个点，求所有点对之间的最长距离。$2\leq n\leq 50000, |x|, |y|\leq 10^4$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，求出这 $n$ 个点的凸包，复杂度可以做到为 $\mathcal{O}(n\log n)$，如何求直径？&lt;/p&gt;
&lt;p&gt;**暴力做法：**可以遍历每个点对，求出最大距离，复杂度为 $\mathcal{O}(n^2)$.&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;p&gt;可以遍历凸包上的边，对每条边 $(a, b)$，去找距离这条边最远的点 $p$。对于 $p$ 点，距离它最远的点，一定是 $a,b$ 中的一个。&lt;/p&gt;
&lt;p&gt;我们发现，若逆时针遍历凸包上的边，那么随着边的转动，对应的最远点也在逆时针旋转。&lt;/p&gt;
&lt;p&gt;因此，我们可以在逆时针枚举边的同时，实时维护最远点，利用单调性，复杂度为 $\mathcal{O}(n)$.&lt;/p&gt;
&lt;h3&gt;算法实现&lt;/h3&gt;
&lt;p&gt;求凸包时，若使用 Andrew 算法，则凸包上的点已经按照逆时针排序了。问题在如何判断下一个点到当前边的距离是否更大。&lt;/p&gt;
&lt;p&gt;一种方法是用点到直线距离公式，但利用叉积的精度更高，也更方便。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Point{
	int x, y;
    //......
	int sqr_len() { return x * x + y * y; }
};
inline int sqr_dis(Point a, Point b) { return (a - b).sqr_len(); }
int Get_Max(int n, Point *ch) {//传入convex-hull
	int ret = 0;
	ch[n] = ch[0];
	int j = 1;
	for(int i = 0; i &amp;#x3C; n; i++) {
		while((ch[i] - ch[j+1]) * (ch[i+1] - ch[j+1]) &gt;
			  (ch[i] - ch[j]) * (ch[i+1] - ch[j]))
			j = (j + 1) % n;
		ret = max(ret, max(sqr_dis(ch[i], ch[j]), sqr_dis(ch[i+1], ch[j])));
	}
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最小矩形覆盖&lt;/h2&gt;
&lt;h3&gt;题意&lt;/h3&gt;
&lt;p&gt;给定一些点的坐标，求能够覆盖所有点的最小面积的矩形。&lt;/p&gt;
&lt;p&gt;求出矩形面积、顶点坐标。$3\leq n\leq 50000$.&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&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;设 $H=|P_0P_3|=\frac{|\overrightarrow{AB}\times \overrightarrow{BU}|}{|AB|}$，$L=\frac{|\overrightarrow{BA}\cdot \overrightarrow{AL}|}{|BA|}$，$R=\frac{|\overrightarrow{AB}\cdot \overrightarrow{BR}|}{|AB|}$.&lt;/p&gt;
&lt;p&gt;则矩形面积为：
$$
S=H\times (L+|\overrightarrow{AB}|+R)
$$
顶点坐标为：
$$
\begin{aligned}
&amp;#x26;\overrightarrow{P_0}&amp;#x26;=&amp;#x26;\overrightarrow A+L\times \frac{\overrightarrow{BA}}{|\overrightarrow{BA}|}\
&amp;#x26;\overrightarrow{P_1}&amp;#x26;=&amp;#x26;\overrightarrow B+R\times \frac{\overrightarrow{AB}}{|\overrightarrow{AB}|}\
&amp;#x26;\overrightarrow{P_2}&amp;#x26;=&amp;#x26;\overrightarrow P_1+H\times \frac{\overrightarrow{P_1R}}{|\overrightarrow{P_1R}|}\
&amp;#x26;\overrightarrow{P_3}&amp;#x26;=&amp;#x26;\overrightarrow P_0+H\times \frac{\overrightarrow{P_0L}}{|\overrightarrow{P_0L}|}
\end{aligned}
$$
若不需要求出顶点坐标，也可以这样表示矩形面积：
$$
S=\frac{|\overrightarrow{AB}\times \overrightarrow{BU}|\times (|\overrightarrow{AD}\cdot\overrightarrow{AB}|+|\overrightarrow{BC}\cdot\overrightarrow{BA}|-|\overrightarrow{AB}\cdot\overrightarrow{BA}|)}{|\overrightarrow{AB}\cdot\overrightarrow{BA}|}
$$
设 $|\overrightarrow{P_0B}|=m$，$|\overrightarrow{AP_1}|=n$ 后易证。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double Get_Max(int n, Point *ch) {
	ch[n] = ch[0];
	int u = 2, l, r = 2;
	//u是距离AB最远的点；在AB为底时，l和r是两个最靠边的点
	double ret = 1e100, H, L, R, S;
	for(int i = 0; i &amp;#x3C; n; i++) {
		Point A = ch[i], B = ch[i+1]; Vec AB = B - A, BA = A - B;
		while((AB * Vec(B, ch[u+1])) &gt;= (AB * Vec(B, ch[u])))
			u = (u + 1) % n;
		while((AB &amp;#x26; Vec(B, ch[r+1])) &gt;= (AB &amp;#x26; Vec(B, ch[r])))
			r = (r + 1) % n;
		if(i == 0) l = r;
		while((AB &amp;#x26; Vec(B, ch[l+1])) &amp;#x3C;= (AB &amp;#x26; Vec(B, ch[l])))
			l = (l + 1) % n;
		H = (AB * Vec(B, ch[u])) / AB.len(); //以AB所在直线为底边，矩形的高
		L = (BA &amp;#x26; Vec(A, ch[l])) / BA.len(); //A距离左侧顶点的距离
		R = (AB &amp;#x26; Vec(B, ch[r])) / AB.len(); //B距离右侧顶点的距离
		S = H * (L + AB.len() + R); //矩形面积
		if(S &amp;#x3C; ret) { //求矩形顶点坐标
			ret = S;
			P[0] = A + L * BA.unit();
			P[1] = B + R * AB.unit();
			P[2] = P[1] + H * (ch[r]-P[1]).unit();
			P[3] = P[0] + H * (ch[l]-P[0]).unit();
		}
	}
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;POJ2079 - Triangle&lt;/h2&gt;
&lt;h3&gt;题意&lt;/h3&gt;
&lt;p&gt;给定平面上的 $n$ 个点，从其中任选三个点作为顶点，求能构成的最大三角形面积。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 50000$.&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;显然，三角形的顶点一定都在这 $n$ 个点的凸包上，所以先求出凸包。&lt;/p&gt;
&lt;p&gt;考虑旋转卡壳。&lt;/p&gt;
&lt;p&gt;这题中不能固定一条边，再枚举另外两个点，因为三角形的边不一定在凸包上。&lt;/p&gt;
&lt;p&gt;因此，先逆时针枚举一个固定点 $i$，再逆时针旋转另外两个顶点 $j$ 和 $k$.·&lt;/p&gt;
&lt;p&gt;由于凸包上的一些单峰性，我们旋转 $j$ 时停止的条件是 $S_{\triangle ijk}$ 最大，停止后固定 $j$，以相同停止条件旋转 $k$.&lt;/p&gt;
&lt;p&gt;$S_{\triangle ijk}$ 最大，是指 $S_{\triangle i,j\operatorname{_last},k}&amp;#x3C;S_{\triangle ijk}$ 且 $S_{\triangle ijk}&gt;S_{\triangle i,j\operatorname{_next},k}$.&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;p&gt;注意特判 $n\leq 2$ 以及凸包大小 $\operatorname{siz}\leq 2$ 的情况。用叉积求面积。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double Get_Max(int n, Point *ch) {
	double ret = 0;
	ch[n] = ch[0];
	int j = 1, k = 2;
	for(int i = 0; i &amp;#x3C; n; i++) {
		while(Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]) &amp;#x3C;
			  Vec(ch[i], ch[j]) * Vec(ch[i], ch[k+1]))
			k = (k + 1) % n;
		while(Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]) &amp;#x3C;
			  Vec(ch[i], ch[j+1]) * Vec(ch[i], ch[k]))
			j = (j + 1) % n;
		ret = max(ret, Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]));
	}
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;半平面交&lt;/h1&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;h2&gt;直线类&lt;/h2&gt;
&lt;p&gt;在计算几何中，用 $(\boldsymbol s, \boldsymbol t)$ 表示直线 ${st}$，一般统一保留向量的左侧半平面。&lt;/p&gt;
&lt;h3&gt;求两直线的交&lt;/h3&gt;
&lt;p&gt;若交点存在，只需求出 $t=\dfrac{|A_sP|}{|A_sA_t|}$，则 $\overrightarrow P=\overrightarrow{A_s}+t\cdot  \overrightarrow{A_sA_t}$.&lt;/p&gt;
&lt;p&gt;注意到 $t=\dfrac{|A_sP|}{|A_sA_t|}=\dfrac{|\overrightarrow{B_sB_t}\times \overrightarrow{B_sA_s}|}{|\overrightarrow{B_sB_t}\times \overrightarrow{A_sA_t}|}$，证明也不难。&lt;/p&gt;
&lt;p&gt;如上图，把 $\overrightarrow{A_sA_t}$ 平移到 $\overrightarrow{B_sC}$ 的位置，则可以把叉乘的模看作平行四边形面积。&lt;/p&gt;
&lt;p&gt;记 $S_1=|\overrightarrow{B_sB_t}\times \overrightarrow{B_sA_s}|$，$S_2=|\overrightarrow{B_sB_t}\times \overrightarrow{A_sA_t}|$，由于两个平行四边形同底，所以有 $\dfrac{S_1}{S_2}=\dfrac{|A_sM|}{|CN|}$.&lt;/p&gt;
&lt;p&gt;又因为 $\triangle A_sMP\sim \triangle CNB_s$，所以 $\dfrac{S_1}{S_2}=\dfrac{|A_sM|}{|CN|}=\dfrac{|A_sP|}{|CB_s|}=\dfrac{|A_sP|}{|A_sA_t|}$.&lt;/p&gt;
&lt;h3&gt;参考代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Line{
	Point s, t;
	Line() {};
	Line(Point a, Point b) : s(a), t(b) {}
	double ang() { return atan2((t - s).y, (t - s).x); };
	Line(double a, double b, double c) { //ax + by + c = 0
		if(sgn(a) == 0) s = Point(0, -c/b), t = Point(1, -c/b);
		else if(sgn(b) == 0) s = Point(-c/a, 0), t = Point(-c/a, 1);
		else s = Point(0, -c/b), t = Point(1, (-c-a)/b);
	}
	friend bool parallel(const Line &amp;#x26;A, const Line &amp;#x26;B) {
		return sgn((A.s - A.t) * (B.s - B.t)) == 0;
	}
	friend bool Calc_intersection(const Line &amp;#x26;A, const Line &amp;#x26;B, Point &amp;#x26;res) {
		if(parallel(A, B)) return false;
		double s1 = (B.t - B.s) * (B.s - A.s);
		double s2 = (B.t - B.s) * (A.t - A.s);
		res = A.s + (A.t - A.s) * (s1 / s2);
		return true;
	}
};
&lt;/code&gt;&lt;/pre&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;p&gt;半平面交在代数意义下，是若干个线性约束条件，每个约束条件形如：
$$
a_ix+b_iy\leq c_i
$$
其中 $a_i, b_i, c_i$ 为常数，且 $a_i$ 和 $b_i$ 不都为零。&lt;/p&gt;
&lt;p&gt;半平面交有下面五种可能情况，每个半平面位于边界直线的阴影一侧。&lt;/p&gt;
&lt;p&gt;$\rm{(iii)}$ 和 $\rm{(v)}$ 比较特殊，我们一般假设产生的交集总是有界或空的。&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;p&gt;注意到，位于半平面交边界上的任何一个点，必定来自某张半平面的边界。&lt;/p&gt;
&lt;p&gt;并且，因为半平面交是凸集，所以每条边界线最多只能为边界贡献一条边。&lt;/p&gt;
&lt;p&gt;因此：&lt;strong&gt;由 $n$ 张半平面相交而成的凸多边形，其边界最多由 $n$ 条边围成&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Sort-and-Incremental Algorithm&lt;/h2&gt;
&lt;p&gt;S&amp;#x26;I 算法，排序增量法。&lt;/p&gt;
&lt;h3&gt;算法思想&lt;/h3&gt;
&lt;p&gt;S&amp;#x26;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;strong&gt;双端队列&lt;/strong&gt;维护。&lt;/p&gt;
&lt;p&gt;下面是一个例子。&lt;/p&gt;
&lt;p&gt;TODO.&lt;/p&gt;
&lt;h3&gt;算法流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;把半平面按照极角序排序，需要 $\mathcal{O}(n\log n)$ 的时间。&lt;/li&gt;
&lt;li&gt;对每个半平面，执行一次增量过程，每次根据需要弹出双端队列的头部或尾部元素。这是线性的，因为每个半平面只会被增加或删除一次。&lt;/li&gt;
&lt;li&gt;最后，通过双端队列中的半平面，在线性时间内求出半平面交（一个凸多边形，用若干顶点描述）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样，我们得到了一个时间复杂度为 $\mathcal{O}(n\log n)$ 的算法，瓶颈在于排序。因此，若题目给定的半平面满足极角序，则我们可以在线性的时间内求出半平面交。&lt;/p&gt;
&lt;h3&gt;极角排序&lt;/h3&gt;
&lt;p&gt;注意判断不要写成 &lt;code&gt;&amp;#x3C;=&lt;/code&gt; 或 &lt;code&gt;&gt;=&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;inline bool cmp(Line A, Line B) {
	//极角相等时，位置靠右的排在前面
	if(!sgn(A.ang - B.ang)) return (A.t - A.s) * (B.t - A.s) &gt; 0;
	return A.ang &amp;#x3C; B.ang;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;求半平面交&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool Halfplane_intersection(int n, Line *hp, Point *p) {
	if(n &amp;#x3C; 3) return false;
	sort(hp, hp + n, cmp);
	Halfplane_unique(n, hp);
	st = 0; ed = 1;
	que[0] = 0; que[1] = 1;
	if(parallel(hp[0], hp[1])) return false;
	Calc_intersection(hp[0], hp[1], p[1]);
	for(int i = 2; i &amp;#x3C; n; i++) {
		while(st &amp;#x3C; ed &amp;#x26;&amp;#x26;
			  sgn((hp[i].t - hp[i].s) * (p[ed] - hp[i].s)) &amp;#x3C; 0)
			ed--;
		while(st &amp;#x3C; ed &amp;#x26;&amp;#x26;
			  sgn((hp[i].t - hp[i].s) * (p[st + 1] - hp[i].s)) &amp;#x3C; 0)
			st++;
		que[++ed] = i;
		assert(ed &gt;= 1);
		if(parallel(hp[i], hp[que[ed - 1]])) return false;
		Calc_intersection(hp[i], hp[que[ed - 1]], p[ed]);
	}
	while(st &amp;#x3C; ed &amp;#x26;&amp;#x26;
		  sgn((hp[que[st]].t - hp[que[st]].s) * (p[ed] - hp[que[st]].s)) &amp;#x3C; 0)
		ed--;
	while(st &amp;#x3C; ed &amp;#x26;&amp;#x26;
		  sgn((hp[que[ed]].t - hp[que[ed]].s) * (p[st + 1] - hp[que[ed]].s)) &amp;#x3C; 0)
		st++;
	if(st + 1 &gt;= ed) return false;
	return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;求凸多边形的顶点&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int Get_convex_hull(Line *hp, Point *p, Point *ch) {
	Calc_intersection(hp[que[st]], hp[que[ed]], p[st]);
	for(int i = 0, j = st; j &amp;#x3C;= ed; i++, j++) ch[i] = p[j];
	return ed - st + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;计算面积&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double Calc_area(int n, Point *ch) {
	double ans = 0;
	for (int i = 2; i &amp;#x3C; n; i++)
		ans += area(ch[0], ch[i - 1], ch[i]);
	return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P3256 [JLOI2013]赛车&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;赛场上一共有 $n$ 辆车，分别称为 $g_1,g_2,...,g_n$.&lt;/p&gt;
&lt;p&gt;赛道是一条无限长的直线。最初，$g_i$ 位于距离起跑线前进 $x_i$ 的位置。比赛开始后，车辆 $g_i$ 将会以 $v_i$ 单位每秒的恒定速度行驶。&lt;/p&gt;
&lt;p&gt;过程中，如果一辆赛车曾经处于领跑位置的话，即没有其他的赛车跑在他的前面，这辆赛车最后就可以得奖。&lt;/p&gt;
&lt;p&gt;求哪些赛车将会得奖。输出赛车编号。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^4$，$0\leq x_i\leq 10^9$，$0\leq v_i\leq 10^9$.&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;可以想到，每辆车的 $x-t$ 图像都是一条直线。在平面直角坐标系上画出这些直线。&lt;/p&gt;
&lt;p&gt;若取每条直线的左侧半平面，再与 $x$ 轴上侧平面、$y$ 轴右侧平面一起求交，则所有凸多边形上的直线可以获奖。&lt;/p&gt;
&lt;p&gt;直接跑排序增量法即可，时间复杂度 $\mathcal{O}(n\log n)$.&lt;/p&gt;
&lt;p&gt;注意到这题 $x_i$ 和 $v_i$ 的值域均为 $10^9$，所以运算过程中的数值会达到 $10^9\times 10^9=10^{18}$，&lt;/p&gt;
&lt;p&gt;因此需要使用 &lt;code&gt;long double&lt;/code&gt;，并且需要 &lt;code&gt;const long double eps = 1e-18&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;p&gt;这道题不能直接去重，因为需要输出获奖车辆的编号，所以用 &lt;code&gt;std::map&lt;/code&gt; 维护。&lt;/p&gt;
&lt;p&gt;同时，还需要在 &lt;code&gt;Line&lt;/code&gt; 类中存储所有重合直线的编号，用 &lt;code&gt;std::vector&lt;/code&gt; 维护。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Line{
	Point s, t;
	vector&amp;#x3C;int&gt; id;
	Line() {};
	Line(Point a, Point b) : s(a), t(b) {}
	Line(Point a, Point b, vector&amp;#x3C;int&gt; c) : s(a), t(b), id(c) {}
	//...
};
//...
signed main() {
	int n, x[MAXN], v[MAXN], N = 0;
	map&amp;#x3C;pair&amp;#x3C;int, int&gt;, vector&amp;#x3C;int&gt; &gt; mp;
	cin &gt;&gt; n;
	for(int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; x[i];
	for(int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; v[i];

	for(int i = 1; i &amp;#x3C;= n; i++) mp[make_pair(x[i], v[i])].push_back(i);
	L[N++] = Line(Point(0, 1), Point(0, 0));
	L[N++] = Line(Point(0, 0), Point(1, 0));
	for(auto it = mp.begin(); it != mp.end(); it++) {
		pair&amp;#x3C;int, int&gt; tmp = it -&gt; first;
		L[N++] = Line(Point(0, tmp.first),
					  Point(1, tmp.first+tmp.second), it -&gt; second);
	}

	Halfplane_intersection(N, L, p);

	int ans[MAXN], cnt_ans = 0;
	for(int i = st + 1; i &amp;#x3C;= ed; i++) {
		for(int j = 0; j &amp;#x3C; L[que[i]].id.size(); j++) {
			ans[cnt_ans++] = L[que[i]].id[j];
		}
	}
	sort(ans, ans + cnt_ans);
	cout &amp;#x3C;&amp;#x3C; cnt_ans &amp;#x3C;&amp;#x3C; endl;
	for(int i = 0; i &amp;#x3C; cnt_ans; i++) cout &amp;#x3C;&amp;#x3C; ans[i] &amp;#x3C;&amp;#x3C; &quot; &quot;; cout &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P4250 [SCOI2015]小凸想跑步&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;小凸晚上喜欢到操场跑步，今天他跑完两圈之后，他玩起了这样一个游戏。&lt;/p&gt;
&lt;p&gt;操场是个凸 $n$ 边形， $n$ 个顶点&lt;strong&gt;按照逆时针&lt;/strong&gt;从 $0$ ∼ $n - 1$ 编号。&lt;/p&gt;
&lt;p&gt;现在小凸&lt;strong&gt;随机&lt;/strong&gt;站在操场中的某个位置，标记为 $p$ 点。将 $p$ 点与 $n$ 个顶点各连一条边，形成 $n$ 个三角形。&lt;/p&gt;
&lt;p&gt;如果这时 $p$ 点， $0$ 号点， $1$ 号点形成的三角形的面积是 $n$ 个三角形中&lt;strong&gt;最小&lt;/strong&gt;的一个，则认为这是一次正确站位。&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;设 $\overrightarrow A=(x_a,y_a)$，$\overrightarrow B=(x_b, y_b)$，$\overrightarrow C=(x_c, y_c)$，$\overrightarrow D=(x_d, y_d)$，$\overrightarrow P=(x, y)$.&lt;/p&gt;
&lt;p&gt;由题意，
$$
\begin{aligned}
|\overrightarrow {AB}\times \overrightarrow{AP}|&amp;#x26;&amp;#x3C;&amp;#x26;|\overrightarrow {CD}\times \overrightarrow{CP}|\
(x_b-x_a)(y-y_a) - (y_b-y_a)(x-x_a) &amp;#x26;&amp;#x3C;&amp;#x26; (x_d-x_c)(y-y_c)-(y_d-y_c)(x-x_c)\
\end{aligned}
$$
展开得，
$$
\begin{aligned}
\text{左式}&amp;#x26;=&amp;#x26;(x_b-x_a)y-x_by_a+x_ay_a-(y_b-y_a)x+x_ay_b-x_ay_a\
\text{右式}&amp;#x26;=&amp;#x26;(x_d-x_c)y-x_dy_c+x_cy_c-(y_d-y_c)x+x_cy_d-x_cy_c
\end{aligned}
$$
合并同类项，
$$
\begin{aligned}
&amp;#x26;(x_b - x_a)(y - y_a) - (y_b - y_a)(x - x_a) &amp;#x3C; (x_d - x_c)(y - y_c) - (y_d - y_c)(x - x_c) \
\Rightarrow &amp;#x26; (x_b - x_a + x_c - x_d)y + (y_a - y_b + y_d - y_c)x + (y_b x_a - x_b y_a - y_d x_c + x_d y_c) &amp;#x3C; 0
\end{aligned}
$$
注意到，这是直线解析式的形式，于是转化为了半平面交问题。&lt;/p&gt;
&lt;h1&gt;一些资料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;题单链接：&lt;a href=&quot;https://www.luogu.com.cn/training/160352&quot;&gt;【2021专题分享】计算几何 - TonyYin - 洛谷 _ 计算机科学教育新生态&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;参考资料：
BERG M, KREVELD M, OVERMARS M H. Computational Geometry: Algorithms and Applications[M]. 2008.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[2] Preparata F P, Shamos M I. Computational geometry: an introduction[M]. Springer Science &amp;#x26; Business Media, 2012.&lt;/p&gt;
&lt;p&gt;[3] TOUSSAINT G T. Solving geometric problems with the rotating calipers[C]//Proc. IEEE Melecon.1983:A10.&lt;/p&gt;
&lt;p&gt;[4] TOUSSAINT G T. The rotating calipers: An efficient, multipurpose, computational tool[C]//The International Conference on Computing Technology and Information Management (ICCTIM).Citeseer,2014:215.&lt;/p&gt;
&lt;p&gt;[5] THOMAS H C, CHARLES,E.LEISERSON,RONALD,L.RIVEST,CLIFFORD,STEIN,殷建平,徐 云,王刚,刘晓光,苏明,邹恒明,王宏志 2013. 算法导论(原书第 3 版). 计算机教育 [J]: 1.&lt;/p&gt;
&lt;p&gt;[6] 董永建. 信息学奥赛一本通[M].科学技术文献出版社,2013.&lt;/p&gt;
&lt;p&gt;[7] PRATA S, 张海龙, 袁国忠. C++ Primer Plus(第 6 版)中文版[M]. 人民邮电出版社, 2012.&lt;/p&gt;
&lt;p&gt;[8] 同济大学数学系.高等数学.下册[M].北京: 高等教育出版社,2014.1-23.\&lt;/p&gt;
&lt;p&gt;[9] 人民教育出版社 课程教材研究所 中学数学教材实验研究组.普通 高中教科书 数学(B 版) 必修 第二册[M].北京:人民教育出版社,2019:131-172.&lt;/p&gt;
&lt;p&gt;https://cp-algorithms.com/geometry/halfplane-intersection.html&lt;/p&gt;</content:encoded></item><item><title>【Codeforces-660E】Different Subsets For All Tuples</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf660e</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf660e</guid><description>来自 zyc 的组合数学好题。</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;一个整数数列，每个元素在 $[1, m]$ 上取值，共有 $m^n$ 种可能的数列。&lt;/p&gt;
&lt;p&gt;对每个数列，分别求出其中本质不同的子序列个数，然后求和，包括空序列。&lt;/p&gt;
&lt;p&gt;对 $10^9+7$ 取模，$1\leq n,m\leq 10^6$.&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;首先考虑空序列，每种数列均有 $1$ 个，共有 $\operatorname{Ans}_1 = m^n$ 个。&lt;/p&gt;
&lt;p&gt;对于非空子序列。考虑固定一个非空子序列，统计包含它的原串个数。&lt;/p&gt;
&lt;p&gt;不妨设其在原序列中的出现位置为 $p_1, p_2, \cdots, p_i$，每个位置上的数值为 $x_1, x_2, \cdots, x_i$.&lt;/p&gt;
&lt;p&gt;为了避免重复，若子序列在原序列中出现多次，则钦定 $p_1\cdots p_i$ 为第一次出现的位置，也就是说，钦定 $p_1\cdots p_i$ 的字典序最小。&lt;/p&gt;
&lt;p&gt;这意味着，$1\sim p_1-1$ 不能出现 $x_1$，$p_1+1\sim p_2-1$ 中不能出现 $x_2$，依次类推，每个 $p$ 都是最靠前的位置。&lt;/p&gt;
&lt;p&gt;开始组合方法推式子，容易得到：
$$
\operatorname{Ans}&lt;em&gt;2 = \sum&lt;/em&gt;{i=1}^{n}m^i\sum_{j=i}^{n}{C_{j-1}^{i-1}(m-1)^{j-i}m^{n-j}}
$$
其中 $i$ 枚举子序列长度，$j$ 枚举子序列最后一个元素的出现位置；&lt;/p&gt;
&lt;p&gt;$m^i$ 为子序列可能的取值数量；$C_{j-1}^{i-1}$ 统计 $p_1\cdots p_n$ 的可能情况数量；&lt;/p&gt;
&lt;p&gt;$(m-1)^{j-i}$ 是填子序列之间的空隙的方法数量；$m^{n-j}$ 代表最后的 $n-j$ 位可以任取。&lt;/p&gt;
&lt;p&gt;现在我们要在 $\mathcal{O}(n)$ 或 $\mathcal{O}(n\log n)$ 的复杂度求出 $\operatorname{Ans}$ 的值，考虑下面的转化。&lt;/p&gt;
&lt;p&gt;合并同类项，
$$
\operatorname{Ans}&lt;em&gt;2 = \sum&lt;/em&gt;{i=1}^{n}\sum_{j=i}^{n}{C_{j-1}^{i-1}(m-1)^{j-i}m^{n-j+i}}
$$
组合数 $C_{j-1}^{i-1}$ 很难处理，考虑二项式定理的形式 $(a+b)^n=\sum\limits_{i=0}^{n}C_n^i\cdot a^i b^{n-i}$ 中，组合数下面的 $n$ 要和 $\sum$ 相同。&lt;/p&gt;
&lt;p&gt;因此，变化 $\sum$ 的形式，并交换两个求和符号，往二项式定理方向转化：
$$
\begin{aligned}
\operatorname{Ans}&lt;em&gt;2 &amp;#x26;= \sum&lt;/em&gt;{i=1}^{n}\sum_{j=i}^{n}{C_{j-1}^{i-1}(m-1)^{j-i}m^{n-j+i}}\
&amp;#x26;= \sum_{i=0}^{n-1}\sum_{j=0}^{n-1}C_j^i(m-1)^{j-i}m^{n-j+i}\
&amp;#x26;= \sum_{j=0}^{n-1}\sum_{i=0}^{j}C_j^i(m-1)^{j-i}m^{n+i-j}
\end{aligned}
$$
二项式定理中，两个指数的和为定值，所以：
$$
\begin{aligned}
\operatorname{Ans}&lt;em&gt;2 &amp;#x26;= \sum&lt;/em&gt;{j=0}^{n-1}\sum_{i=0}^{j}C_j^i(m-1)^{j-i}m^{n+i-j}\
&amp;#x26;= \sum_{j=0}^{n-1}m^{n-j}\sum_{i=0}^{j}C_j^i(m-1)^{j-i}m^{i}\
&amp;#x26;= \sum_{j=0}^{n-1}m^{n-j}[(m-1)+m]^{j}\
&amp;#x26;= \sum_{j=0}^{n-1}m^{n-j}(2m-1)^{j}\
\end{aligned}
$$
这个可以在 $\mathcal{O}(n)$ 时间复杂度内求出，最终答案的形式为：
$$
\operatorname{Ans}=\textcolor{gray}{\operatorname{Ans}_1+\operatorname{Ans}&lt;em&gt;2}=m^n+\sum&lt;/em&gt;{j=0}^{n-1}m^{n-j}(2m-1)^{j}
$$&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 1e6 + 10, mod = 1e9 + 7;
int n, m, ans, Pow1[MAXN] = {1}, Pow2[MAXN] = {1};
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;m);
	for(int i = 1; i &amp;#x3C;= n; i++) 
        Pow1[i] = Pow1[i - 1] * m % mod, Pow2[i] = Pow2[i - 1] * (2 * m - 1) % mod;
	for(int i = 0; i &amp;#x3C; n; i++) 
        ans = (ans + Pow1[n - i] * Pow2[i] % mod) % mod;
	printf(&quot;%lld\n&quot;, (ans + Pow1[n]) % mod) ;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【ZROI–21noip赛前–Day15】套路题</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi2141</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi2141</guid><description>一道概率 DP 练习题。</description><pubDate>Tue, 09 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;给定一个长度为 $n$ 的序列 $a_1, a_2\cdots, a_n$，现在要把它划分成 $k$ 段，问每段平均数的最小值最大能是多少。&lt;/p&gt;
&lt;p&gt;精度上，要求误差 $\leq 0.001$.&lt;/p&gt;
&lt;p&gt;$1\leq k\leq n\leq 2\times 10^5$，$1\leq a_i\leq 100$.&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;要让最小值最大，想到二分答案，但 $\rm{check}$ 部分不是很好想。&lt;/p&gt;
&lt;p&gt;容易想到时间复杂度较劣的DP做法。&lt;/p&gt;
&lt;h2&gt;暴力DP&lt;/h2&gt;
&lt;p&gt;设 $f_{i, j}$ 表示，处理完前 $i$ 个数，分成了 $j$ 段，每段平均数最小值的&lt;strong&gt;最大可能值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;预处理前缀和 $s_i=\sum_{j=1}^{i}{a_i}$，则有转移方程：&lt;/p&gt;
&lt;p&gt;$$
f_{i, j}=\max_{k=1}^{i}{\min{{f_{k, j-1}, \frac{s_i-s_k}{i-k}}}}
$$&lt;/p&gt;
&lt;p&gt;表示 $[1, k]\cup[k+1, i] \rightarrow [1, i]$ 的过程，要和新加入的 $[k+1, i]$ 的平均值取 $\min$.&lt;/p&gt;
&lt;p&gt;这样 DP 的时间复杂度为 $\mathcal{O}(n^3)$，不是很容易继续优化。&lt;/p&gt;
&lt;h2&gt;二分答案&lt;/h2&gt;
&lt;p&gt;假设二分出了一个值 $x$，需要 $\rm{check}$ 其是否可能作为答案。&lt;/p&gt;
&lt;p&gt;我们把序列中的每个数都减去 $x$ 后，&lt;strong&gt;一段的平均数 $\geq x$ 当且仅当这段的元素和 $\geq 0$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;在每次 $\rm{check}$ 时，先算出数列 $b_i=a_i-x$，再对 $b_i$ 计算前缀和 $s_i$。&lt;/p&gt;
&lt;p&gt;若序列可以分为 $k$ 个平均值 $\geq x$ 的段，则说明在序列 $s$ 上，以一个 $&gt;0$ 的数为开头，且以 $s_n$ 为结尾的&lt;strong&gt;最长不下降子序列&lt;/strong&gt;长度 $\geq k$.&lt;/p&gt;
&lt;p&gt;于是问题变成：如何快速求这样的最长不下降子序列。&lt;/p&gt;
&lt;p&gt;在求出最长不下降子序列长度之后，$\rm{check}$ 操作就结束了。&lt;/p&gt;
&lt;h2&gt;最长不下降子序列&lt;/h2&gt;
&lt;p&gt;求序列 $s$ 中，以非负整数开头，以 $s_n$ 结尾的最长不下降子序列。&lt;/p&gt;
&lt;p&gt;设 $f_i$ 表示以 $i$ 结尾的最长不下降子序列长度，转移显然：&lt;/p&gt;
&lt;p&gt;$$
f_i=\max_{j&amp;#x3C;i, 0&amp;#x3C;a_j&amp;#x3C;a_i}(f_j+1)
$$&lt;/p&gt;
&lt;p&gt;暴力做是 $\mathcal{O}(n^2)$ 的，可以用&lt;strong&gt;树状数组&lt;/strong&gt;优化到 $\mathcal{O}(n\log_2 n)$.&lt;/p&gt;
&lt;p&gt;先把点按照权值排序，树状数组以序列原本的下标为下标。&lt;/p&gt;
&lt;p&gt;在按权值排序后的序列顺序递推，每次利用树状数组求前缀最大值即可。&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;p&gt;实现的时候先整体 $\times 10000$，用整数计算。不过也可以直接用小数算。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 2e5 + 10, inf = 0x3f3f3f3f3f3f3f3f;
int n, k, a[MAXN];
class BIT{
	private:
		int c[MAXN], N = 2e5;
	public:
		void init(){ memset(c, 0xc0, sizeof(c)); }
		inline int lowbit(int x) { return x &amp;#x26; (-x); }
		inline void update(int x, int w) {
			for(; x &amp;#x3C;= N; x += lowbit(x)) c[x] = max(c[x], w);
		}
		inline int query(int x) {
			int ret = -inf;
			for(; x; x -= lowbit(x)) ret = max(ret, c[x]);
			return ret;
		}
} Bit;
int pos[MAXN];
pair&amp;#x3C;int, int&gt; s[MAXN];
bool check(int mid) {
	int cnt = 0; Bit.init();
	s[0] = make_pair(0, 0);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		s[i].first = s[i - 1].first + a[i] - mid;
		s[i].second = i;
	}
	sort(s, s + n + 1);
	pos[s[0].second] = ++cnt;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		if(s[i].first != s[i - 1].first) ++cnt;
		pos[s[i].second] = cnt;
	}
	//求：每个元素都&gt;=0的最长上升子序列长度
	Bit.update(pos[0], 0);
	for(int i = 1; i &amp;#x3C; n; i++) {
		int ret = Bit.query(pos[i] - 1);
		Bit.update(pos[i], ret + 1);
	}
	return (Bit.query(pos[n] - 1) + 1 &gt;= k);
}
signed main() {
	ios::sync_with_stdio(false);
	cin &gt;&gt; n &gt;&gt; k;
	for(int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; a[i];
	for(int i = 1; i &amp;#x3C;= n; i++) a[i] *= 10000;
	int l = 0, r = 100 * 10000, mid, ans;
	while(l &amp;#x3C;= r) {
		mid = (l + r) &gt;&gt; 1;
		if(check(mid)) {
			ans = mid;
			l = mid + 1;
		} else r = mid - 1;
	}
	cout &amp;#x3C;&amp;#x3C; ans / 10000.0 &amp;#x3C;&amp;#x3C; endl;
 	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P4147】玉蟾宫</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4147</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4147</guid><description>求解最大子矩形问题。</description><pubDate>Sat, 06 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题意&lt;/h1&gt;
&lt;p&gt;给定 $n\times m$ 的矩阵，其中的若干个格子是障碍。&lt;/p&gt;
&lt;p&gt;求最大的不包含障碍点的子矩形面积。&lt;/p&gt;
&lt;p&gt;$1\leq n, m\leq 1000$.&lt;/p&gt;
&lt;h1&gt;法一&lt;/h1&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;每个可能作为答案的矩形，其四周一定都紧邻障碍块。&lt;/p&gt;
&lt;p&gt;考虑何时将一个矩形统计进答案，能在 $\mathcal{O}(n^2)$ 的时间复杂度要求内做到不重不漏。&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;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;/p&gt;
&lt;p&gt;先枚举行，再枚举列。对于非障碍点 $(i, j)$，若 $(i-1, j)$ 也不是障碍点，则更新这条高线能左右移动的距离。&lt;/p&gt;
&lt;p&gt;计算 $(i, j)$ 对应的矩形面积，用最大宽度 $\times$ 高即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e3 + 10;
int n, m, mapp[MAXN][MAXN];
int l[MAXN][MAXN], r[MAXN][MAXN];//(i, j)向左或向右走，能到达的最靠边的点的纵坐标
int up[MAXN][MAXN];//(i, j)向上走最多走多少步（不能走是1步
int main() {
	scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;m);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = 1; j &amp;#x3C;= m; j++) {
			char ch; cin &gt;&gt; ch;
			mapp[i][j] = up[i][j] = (ch == &apos;F&apos;);
			l[i][j] = r[i][j] = j;
		}
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = 2; j &amp;#x3C;= m; j++) {
			if(mapp[i][j] == 1 &amp;#x26;&amp;#x26; mapp[i][j - 1] == 1) l[i][j] = l[i][j - 1];
		}
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = m - 1; j &gt;= 1; j--) {
			if(mapp[i][j] == 1 &amp;#x26;&amp;#x26; mapp[i][j + 1] == 1) r[i][j] = r[i][j + 1];
		}
	}
	int ans = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = 1; j &amp;#x3C;= m; j++) {
			//要维护向上一整段的 左边/右边限制
			if(i &gt; 1 &amp;#x26;&amp;#x26; mapp[i][j] == 1 &amp;#x26;&amp;#x26; mapp[i - 1][j] == 1) {
				r[i][j] = min(r[i][j], r[i - 1][j]);
				l[i][j] = max(l[i][j], l[i - 1][j]);
				up[i][j] = up[i - 1][j] + 1;
			}
			ans = max(ans, (r[i][j] - l[i][j] + 1) * up[i][j]);
		}
	}
	cout &amp;#x3C;&amp;#x3C; 3 * ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;法二&lt;/h1&gt;
&lt;p&gt;类似地，考虑&lt;strong&gt;单调栈&lt;/strong&gt;做法。&lt;/p&gt;
&lt;p&gt;定义 $f(i, j)$ 表示从 $(i, j)$ 向上一直走，直到碰到障碍，&lt;strong&gt;最多能走多少格&lt;/strong&gt;。容易预处理。&lt;/p&gt;
&lt;p&gt;分开处理每行。&lt;strong&gt;单调栈维护 $f$ 递增&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体地，设当前正在处理第 $i$ 行，遇到了点 $(i, j)$.&lt;/p&gt;
&lt;p&gt;向单调栈内插入 $(i, j)$ 前，要把所有 $f_{i, k}&gt;f_{i, j}$ 的点弹出。弹出 $f_{i, k}$ 时，将面积为 $f_{i,k}\times (j-k)$ 的矩形统计。&lt;/p&gt;
&lt;p&gt;如图。&lt;/p&gt;</content:encoded></item><item><title>【Codeforces-167B】Wizards and Huge Prize</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf167b</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf167b</guid><description>一道概率 DP 练习题。</description><pubDate>Fri, 05 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/CF167B&quot;&gt;CF167B - Wizards and Huge Prize - 洛谷&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;最开始，你有 $M$ 的容积。有 $n$ 轮比赛，对于第 $i$ 场比赛，你获胜的概率为 $p_i$.&lt;/p&gt;
&lt;p&gt;比赛的奖励分为&lt;strong&gt;两种&lt;/strong&gt;：一种是增加 $a_i$ 的容积，一种是给你 $1$ 个奖品。&lt;/p&gt;
&lt;p&gt;一种赢比赛的&lt;strong&gt;合法方案&lt;/strong&gt;，需要满足：$n$ 轮比赛结束时，&lt;strong&gt;总容积 $\geq $ 获得的奖品数量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;求获胜场次 $\geq l$，且方案合法的&lt;strong&gt;概率&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$1\leq M, n, a_i\leq 200$，$0\leq p_i\leq 1$.&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;概率 DP。&lt;/p&gt;
&lt;p&gt;显然，获得一个奖品等价于容积 $-1$，&lt;strong&gt;方案合法 $\Leftrightarrow$ $n$ 轮结束后的容积非负&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设 $\operatorname{dp}_{i, j, k}$ 表示：&lt;strong&gt;前 $i$ 轮比赛后，在其中的 $j$ 场中获胜，当前可用容积为 $k$&lt;/strong&gt; 发生的概率。&lt;/p&gt;
&lt;p&gt;为了方便表示，允许下标为负，则：
$$
\operatorname{Ans} = \sum_{i=l}^{n}\sum_{j=0}^{+\infty}{\operatorname{dp}_{n,i,j}}
$$
第三维数组的大小？&lt;/p&gt;
&lt;p&gt;注意到，每次操作最多减 $1$，如果容积 $&gt;=n$ ，就不可能不合法了，都和 $n$ 的状态等价。&lt;/p&gt;
&lt;p&gt;所以有意义的可用容积在 $[-n, n]$ 范围内，空间开 $2\times n$ 即可，答案的表达式也可以改写为：
$$
\operatorname{Ans} = \sum_{i=l}^{n}\sum_{j=0}^{n}{\operatorname{dp}_{n,i,j}}
$$
考虑如何转移？&lt;/p&gt;
&lt;p&gt;刷表，状态 $(i, j, k)$ 只会贡献到 $(i+1, j, k)$ 或 $(i+1, j+1, \min{n, k+a_{i+1}})$.&lt;/p&gt;
&lt;p&gt;两种转移分别对应 &lt;strong&gt;赢/不赢&lt;/strong&gt; 第 $i+1$ 轮，具体地：
$$
\begin{array}{rcl}
(1-p_{i+1})\times \operatorname{dp}&lt;em&gt;{i,j,k}&amp;#x26;\rightarrow &amp;#x26;\operatorname{dp}&lt;/em&gt;{i+1,j,k}\
p_{i+1}\times \operatorname{dp}&lt;em&gt;{i,j,k}&amp;#x26;\rightarrow &amp;#x26;\operatorname{dp}&lt;/em&gt;{i+1, j+1, \min{n,k+a_{i+1}}}
\end{array}
$$
时空复杂度均为 $\mathcal{O}(n^3)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;实现时，注意将第三维坐标整体 $+n$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int N = 200;

dp[0][0][N + K] = 1.0;
for(int i = 0; i &amp;#x3C; n; i++) {
    for(int j = 0; j &amp;#x3C;= i; j++) {
        for(int k = 0; k &amp;#x3C;= (N &amp;#x3C;&amp;#x3C; 1); k++) {
            dp[i + 1][j][k] += (1.0 - p[i + 1]) * dp[i][j][k];
            int nxt = min((N &amp;#x3C;&amp;#x3C; 1), k + a[i + 1]);
            if(nxt &gt;= 0) dp[i + 1][j + 1][nxt] += p[i + 1] * dp[i][j][k];
        }
    }
}

for(int i = l; i &amp;#x3C;= n; i++) {
    for(int j = N; j &amp;#x3C;= (N &amp;#x3C;&amp;#x3C; 1); j++) {
        ans += dp[n][i][j];
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P2839】[国家集训队]middle</title><link>https://www.tonyyin0418.com/blog/oi-solution/p2839</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p2839</guid><description>算法题解</description><pubDate>Thu, 04 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;中位数&lt;/strong&gt;：给定一个长度为 $n$，下标从 $0$ 开始的序列 $a$，设其排序之后为 $b$，那么中位数为 $b_{\lfloor\frac{n}{2}\rfloor}$.&lt;/p&gt;
&lt;p&gt;给定一个长度为 $n$ 的，下标从 $0$ 开始的序列 $s$，回答 $Q$ 次形如 $(a, b, c, d)$ 的询问，表示：&lt;/p&gt;
&lt;p&gt;保证 $a_i&amp;#x3C;b_i&amp;#x3C;c_i&amp;#x3C;d_i$，多测，强制在线。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 20000$，$1\leq Q\leq 25000$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;由于中位数的定义中，下标从 $0$ 开始，一些细节问题需要特别考虑。&lt;/p&gt;
&lt;p&gt;对于一个 $x$，把 $&gt;=x$ 的元素赋值为 $1$，否则赋值为 $-1$。则区间元素和 $&gt;=0$，当且仅当中位数 $\operatorname{mid}&gt;=x$.&lt;/p&gt;
&lt;p&gt;考虑对每次询问二分。&lt;/p&gt;
&lt;p&gt;在处理询问之前，对于每个 $x$ 预处理出一颗线段树，维护区间内的最大前缀、最大后缀、区间元素和。&lt;/p&gt;
&lt;p&gt;在询问 $(a,b,c,d)$ 中，$[b+1,c]$ 这段是必须选的，再加上 $[a, b]$ 中的最大后缀，和 $[c,d]$ 中的最大前缀即可。&lt;/p&gt;
&lt;p&gt;注意到离散化后，每当 $x$ 的值增加 $1$ 时，只会影响序列中的一个值，因此建立可持久化线段树。&lt;/p&gt;
&lt;p&gt;$x$ 每增加 $1$，线段树只会多加一条链，空间复杂度为 $\mathcal{O}(Q\log n)$ 级别。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;不难实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 20010, inf = 0x3f3f3f3f3f3f3f3f;
int n;
struct Node{
	int v, pos;
} a[MAXN];
bool cmp(Node A, Node B) { return A.v &amp;#x3C; B.v; }
struct Point{ //可持久化线段树
	int lv, rv, sum; //区间内最大前缀和，最大后缀和，区间元素和
	int lc, rc; //左右儿子编号
} t[MAXN &amp;#x3C;&amp;#x3C; 5];
int root[MAXN], p_cnt = 0;
#define ls(x) (t[x].lc)
#define rs(x) (t[x].rc)
void push_up(int rt) {
	t[rt].sum = t[ls(rt)].sum + t[rs(rt)].sum;
	t[rt].lv = max(t[ls(rt)].lv, t[ls(rt)].sum + t[rs(rt)].lv);
	t[rt].rv = max(t[rs(rt)].rv, t[rs(rt)].sum + t[ls(rt)].rv);
}
void build(int &amp;#x26;rt, int l, int r) {
	rt = ++p_cnt;
	if(l == r) {
		//初始的 x = 0，所以每个点权值都是 1
		t[rt].lv = t[rt].rv = t[rt].sum = 1;
		return;
	}
	int mid = (l + r) &gt;&gt; 1;
	build(ls(rt), l, mid);
	build(rs(rt), mid + 1, r);
	push_up(rt);
}
void update(int &amp;#x26;rt, int his, int l, int r, int x, int v) {
	//以 his 为基础进行修改，把 x -&gt; v
	rt = ++p_cnt; t[rt] = t[his];
	if(l == r) {
		t[rt].lv = t[rt].rv = t[rt].sum = v;
		return;
	}
	int mid = (l + r) &gt;&gt; 1;
	if(x &amp;#x3C;= mid) update(ls(rt), ls(his), l, mid, x, v);
	else update(rs(rt), rs(his), mid + 1, r, x, v);
	push_up(rt);
}
int query_s(int rt, int l, int r, int x, int y) {
	//询问区间[x ,y]的和
	if(x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y) return t[rt].sum;
	int mid = (l + r) &gt;&gt; 1, ret = 0;
	if(x &amp;#x3C;= mid) ret += query_s(ls(rt), l, mid, x, y);
	if(y &gt; mid) ret += query_s(rs(rt), mid + 1, r, x, y);
	return ret;
}
int query_l(int rt, int l, int r, int x, int y) {
	//询问区间[x, y]最大的前缀和
	if(x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y) return t[rt].lv;
	int mid = (l + r) &gt;&gt; 1;
	if(y &amp;#x3C;= mid) return query_l(ls(rt), l, mid, x, y); //注意要分类讨论
	else if(x &gt; mid) return query_l(rs(rt), mid + 1, r, x, y);
	else return max(
        query_l(ls(rt), l, mid, x, mid), 
        query_s(ls(rt), l, mid, x, mid) + query_l(rs(rt), mid + 1, r, mid + 1, y)
    );
}
int query_r(int rt, int l, int r, int x, int y) {
	//询问区间[x, y]最大的后缀和
	if(x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y) return t[rt].rv;
	int mid = (l + r) &gt;&gt; 1;
	if(y &amp;#x3C;= mid) return query_r(ls(rt), l, mid, x, y);
	else if(x &gt; mid) return query_r(rs(rt), mid + 1, r, x, y);
	else return max(
        query_r(rs(rt), mid + 1, r, mid + 1, y), 
        query_s(rs(rt), mid + 1, r, mid + 1, y) + query_r(ls(rt), l, mid, x, mid)
    );
}
bool check(int x, int a, int b, int c, int d) {
	//是否存在端点 a&amp;#x3C;=l&amp;#x3C;=b, c&amp;#x3C;=r&amp;#x3C;=d 满足 [l, r] 的元素和 &gt;= 0
	int sum = 0;
	if(c &gt;= b + 2) { //b, c中间有一段一定要选
		sum += query_s(root[x], 1, n, b + 1, c - 1);
	}
	sum += query_r(root[x], 1, n, a, b); //后缀 最大连续段
	sum += query_l(root[x], 1, n, c, d); //前缀 最大连续段
	return (sum &gt;= 0);
}
signed main() {
	n = read();
	for(int i = 1; i &amp;#x3C;= n; i++) a[i].v = read(), a[i].pos = i;
	sort(a + 1, a + n + 1, cmp); //离散化
	build(root[1], 1, n);
	for(int i = 2; i &amp;#x3C;= n + 1; i++) {
		update(root[i], root[i - 1], 1, n, a[i - 1].pos, -1);
	}
	int Q = read(), lstans = 0;
	while(Q--) {
		int q[4];
		q[0] = (read() + lstans) % n + 1;
		q[1] = (read() + lstans) % n + 1;
		q[2] = (read() + lstans) % n + 1;
		q[3] = (read() + lstans) % n + 1;
		sort(q, q + 4);
		int l = 1, r = n, mid, ans = -1;
		while(l &amp;#x3C;= r) {
			mid = (l + r) &gt;&gt; 1;
			if(check(mid, q[0], q[1], q[2], q[3])) {
				l = mid + 1;
				ans = mid;
			} else r = mid - 1;
		}
		printf(&quot;%lld\n&quot;, a[ans].v); lstans = a[ans].v;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>NOIP2021 – 赛前集训</title><link>https://www.tonyyin0418.com/blog/oi-solution/noip2021</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/noip2021</guid><description>NOIP2021 赛前集训的若干题解记录</description><pubDate>Sun, 31 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;10/25 - 21noip赛前20天 day8&lt;/h1&gt;
&lt;p&gt;T1构造题，T2线代，T3是树上问题，T4字符串。&lt;/p&gt;
&lt;p&gt;选择先做T2，首先一眼有 $50$ 分的做法，直接暴力枚举三元组，放到一个矩阵里，求三阶行列式，若行列式不为 $0$，则三个向量是线性无关的。&lt;/p&gt;
&lt;p&gt;后面又想到去把 $n$ 个向量都扔到一个矩阵里，每次枚举前两个，快速找到第三个，但并不会快速找到第三个的方法，只能是一些常数级别的优化。赛时发布通知，说 T1 为了防止被错误做法卡过去，把数据加强了。就去看 T1 了。&lt;/p&gt;
&lt;p&gt;先自己手算了前 $20$ 的，发现方案不唯一，于是就都列出来，然后找有没有统一的构造方法。&lt;/p&gt;
&lt;p&gt;先发现对于偶数 $x=2\cdot y$，可以递归到子问题 $y$，然后把每个数都 $\times 2$ ，一定合法。&lt;/p&gt;
&lt;p&gt;然后就开始找奇数的规律。发现要想不和递归后的问题重复，就每次减去 $3$ 的最高次幂，然后再递归子问题。&lt;/p&gt;
&lt;p&gt;不确定对不对，因为 $100$ 分要高精度，先写了个 $70$ 分数据范围的验证正确性。&lt;/p&gt;
&lt;p&gt;构造题验证正确性要自己写 $\rm{checker}$，考场上写错了，调了半天 $\rm{checker}$，差点以为是自己解法错了。&lt;/p&gt;
&lt;p&gt;时间也不是很多了，就没去写高精度，先去看了看后面的题。大概花了半个小时，得到了自己暴力都不会的结论。&lt;/p&gt;
&lt;p&gt;最后还剩二十分钟去写高精度，没调出来。&lt;/p&gt;
&lt;p&gt;估分 $70+50+0+0$，得分 $0+50+0+0$。&lt;/p&gt;
&lt;p&gt;第一题输出格式错了，后面因为不舒服就没检查，改完上去就有 $70$ 分，加个高精就是 $100$ 分。&lt;/p&gt;
&lt;p&gt;排名：$92/156$。&lt;/p&gt;
&lt;h1&gt;10/26 - 21noip赛前20天 day9&lt;/h1&gt;
&lt;p&gt;按题目编号顺序开题。&lt;/p&gt;
&lt;p&gt;T1又是奇怪的结论题，手造数据试了试，猜了一个：$\operatorname{Ans}=\max($白色连通块个数，黑色连通块个数$)$.&lt;/p&gt;
&lt;p&gt;试了好几组自己造的数据，都是对的，就交上去了，去看了 T2。耗时 $\rm{30min}$.&lt;/p&gt;
&lt;p&gt;观察数据范围发现，因为取模的特殊性，打暴力都要写高精度，肯定是直接找到策略贪心地选。&lt;/p&gt;
&lt;p&gt;思考一下发现，对于每个数而言，一定先做操作三，其次是连续的操作二，最后做操作一。想到这，就感觉快做出来了。&lt;/p&gt;
&lt;p&gt;想到赋值操作只保留最大的那次，转化为加法操作。对一次加法操作，转化为乘上 $\frac{a}{b}$，然而并不知道连续的加法操作如何处理，就没继续往这个方向想了。&lt;/p&gt;
&lt;p&gt;赛后才知道，距离正解只差一步了，因为加法一定是先加大的数，再加小的数，所以排序后，加上的是连续段。把每个加法操作转化为：作用在前一次加法操作后的数上的乘法操作即可。然后直接全局找最大的 $m$ 次乘法操作即可计算。&lt;/p&gt;
&lt;p&gt;又想了一会还是不会（因为正确的方向已经被自己否定掉了），写了 $30$ 部分分，在 1h30min 的时候回到了T1。因为是猜的结论，想拿对拍验证一下，结果&lt;code&gt;WA on Test #1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;看了一下，确实是个反例，而且直接随机数据就很容易出反例。开始重新想 T1。&lt;/p&gt;
&lt;p&gt;模拟了一下过程，如果把连通块缩成一个点，构成的新树一定是：一层黑，一层白，一层黑，一层白这样交替下去的。然而还是不会，就在新树上找规律。&lt;/p&gt;
&lt;p&gt;想在新树上DP，想了个状态+转移方程写出来了，对拍能找到反例。于是就循环在找反例、改DP上 1h 左右，最后终于找到一组反例，设定的状态完全无法解决。突然想到把上面两种错误做法合到一起，取最优解，交上去，赛后发现也可以获得 $70$ 分的高分。&lt;/p&gt;
&lt;p&gt;但毕竟没有正确性，只能再重新想。在还剩30min的时候猜出：答案为缩点后的树的直径，写完之后拍不出来反例。感觉很神奇，就把前面的删掉，只保留了树的直径。&lt;/p&gt;
&lt;p&gt;T3T4没时间了。&lt;/p&gt;
&lt;p&gt;估分 $100+30+0+0$，得分 $100+35+0+0$.&lt;/p&gt;
&lt;p&gt;T1对拍假做法花了太长时间，导致没看后面的题。不过T3T4没几个人拿到部分分。&lt;/p&gt;
&lt;p&gt;所以在对拍出错解之后，应该先检查一下正确性再继续调代码，不然如果最后发现做法假了，就会浪费很多时间。&lt;/p&gt;
&lt;p&gt;排名：$91/158$。&lt;/p&gt;
&lt;h1&gt;10/31 - 21秋季noip10连 day8&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/10/31/f4083d3035a4e.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;先看了T1。30min之后连样例都看不懂，还和期望有关，估计不会，就直接跳了。&lt;/p&gt;
&lt;p&gt;T2，首先注意到数据范围中 $1\leq Q\leq 15$，想要容斥处理。手推了一下只会处理两个区间的交集，三个往上就不行了。先写了个10分暴力，1.5h的时候写完。&lt;/p&gt;
&lt;p&gt;观察了一下榜，发现T4有不少提交，把T3和T4题意看完之后，先去尝试做了T4。对没有修改的情况，又是结论题。试了几组数据完全找不到规律，暴力也不知道每次该怎么修改，2h时去写C。&lt;/p&gt;
&lt;p&gt;看完题感觉和数论相关，但我并不会快速判断能否到达源点，也想不到什么把状态转化的方法。对暴力的部分，想了一个分层图的做法，要分 $2^n$ 层，时间复杂度不是很好，估计也写不出来，就继续回去推T2了。&lt;/p&gt;
&lt;p&gt;$\rm{sub2}$ 中 $1\leq Q\leq 8$。想到了一个方法是按区间端点离散化，枚举每段内模 $9$ 的余数，再判断是否合法，复杂度分析上是 $10^{2Q}\cdot \log Q$ 的，但如果在 DFS 的同时判断当前区间是否合法，并且区间端点有重合的话，也许有 $30$ 分。&lt;/p&gt;
&lt;p&gt;3.5h时通过特别水的大样例。&lt;/p&gt;
&lt;p&gt;再回来看T3，发现打暴力的 $40$ 分并不需要分层图，只需要在 DFS 时加一维：当前所学习到的移动方式即可，用二进制压缩的方式存到一个数里面。这样 DFS 状态数最多是 $(2|x_i|)^2\times 2^n=400\times 400\times 32=5120000$.&lt;/p&gt;
&lt;p&gt;打标记的时候习惯性的加了个回溯，找了半天才发现不需要回溯。删掉之后在本地跑样例会爆栈，终端报 &lt;code&gt;segmentation fault&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/10/31/edbdc910f5131.png&quot; alt=&quot;segmentation fault&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时候就剩五分钟了，静态查错也看不出来啥问题。突然想到可能是Mac的本地栈空间比较小，在OJ上交了一下，确实能正常运行样例。&lt;/p&gt;
&lt;p&gt;最后两分钟发现 T3 无解的情况要输出 $-1$ 而不是最大值，补了个特判交上去。&lt;/p&gt;
&lt;p&gt;估分：$0+(10\sim 30)+40+0$，得分 $0+10+50+0$.&lt;/p&gt;
&lt;p&gt;T2有 subtask 绑定，$30$ 分的部分超时了一个点。T3没有 subtask 绑定，输出的 $-1$ 多拿到了 $10$ 分。&lt;/p&gt;
&lt;p&gt;排名：$39/168$，大部分人总分都没上 $100$。&lt;/p&gt;
&lt;h1&gt;11/07 - 21秋季noip10连 day9&lt;/h1&gt;
&lt;p&gt;按顺序开题，第一题看懂之后就能有一些思路。&lt;/p&gt;
&lt;p&gt;先想到的是预处理出 $f_{i, j}$，表示对于第 $i$ 种狗子，选其的任意一个子集，子集的智力和 $\bmod m$ 为 $j$ 的方案数。&lt;/p&gt;
&lt;p&gt;对于前 $60\rm{pts}$ 的数据， $a_i\leq 10$，$f$ 可以用组合数直接算。1h左右想到这里。&lt;/p&gt;
&lt;p&gt;之后，每种狗子只能选一种 $\bmod m$ 的取值，于是就变成分组背包。耗时1.5h，估分 $60$。&lt;/p&gt;
&lt;p&gt;然后想满分做法，当 $a_i$ 增大时，复杂度瓶颈在预处理部分，想了好久不知道咋弄， 就去看 T2 了。&lt;/p&gt;
&lt;p&gt;T2题面里说是字符串，读了一遍就知道，这题和字符串的算法没关系。&lt;/p&gt;
&lt;p&gt;先手模了一下数据范围里的特殊性质，发现和奇偶性有关。大概是先倒着隔一个选一个，再正着也来一遍。&lt;/p&gt;
&lt;p&gt;又想到在 $a_i$ 和 $a_{i+1}$ 之间的这段，一定是整体正或者负，所以就每段只留 $2$ 个数，写了个大一点的数据手模。&lt;/p&gt;
&lt;p&gt;发现规律还是类似的，倒着的时候字符串内部为正序，正着的时候字符串内部为倒序。就直接模拟，线性复杂度，估分 $100$，还剩 1.5h。&lt;/p&gt;
&lt;p&gt;去看了一遍T3，没什么思路，先跳到T4了。&lt;/p&gt;
&lt;p&gt;看懂题之后 $n^2$ 的暴力很显然，有 $30$ 分。还有 $10$ 分的特殊性质，用前缀和优化一下就好了。估分 $40$，写完还剩1h.&lt;/p&gt;
&lt;p&gt;T3还是不会，返回去看T1。往组合上面想，不可避免的要枚举子集大小，复杂度里直接带着 $10^9$。还剩 20min 的时候终于想到能利用周期性质的做法了。因为只需要考虑一个周期与前面已经处理好的连续周期之间的合并，每次转移的方法一定是相同的，考虑矩阵快速幂。&lt;/p&gt;
&lt;p&gt;复杂度是 $\mathcal{O}(m^3\log_2 a_i)$，往里面带数值的时候把 $m, n$ 弄混了，导致 $20^3\rightarrow 100^3$，没看错也写不完了。&lt;/p&gt;
&lt;p&gt;赛后证明这种矩阵快速幂做法可行，但不必要这么麻烦。存在只用矩阵快速幂、不用分组背包的做法，也有都不用的做法。&lt;/p&gt;
&lt;p&gt;估分 $60+100+0+40$，得分 $60+100+0+40$，排名 $77/168$.&lt;/p&gt;
&lt;p&gt;T1一大堆人过，赛时思路卡在一个方向上的时间太久了。&lt;/p&gt;
&lt;h1&gt;11/08 - 21noip赛前20天 day15&lt;/h1&gt;
&lt;p&gt;先读了每道题。做T1，拿到之后只想到 $\mathcal{O}(n^2m)$ 的暴力，空间复杂度也要 $\mathcal{O}(n^2)$，能拿到 $30$ 分。看完题+写完这部分花了1h.&lt;/p&gt;
&lt;p&gt;继续想正解，题目最下面说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**附：**需要用 &lt;code&gt;__builtin_popcountll()&lt;/code&gt; 来计算 64 位整数的 popcount，而不是 &lt;code&gt;__builtin_popcount()&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;等于 $64$ 的是 $m$，是操作的次数。想的过于复杂导致没做出来，过了30min去看T2。&lt;/p&gt;
&lt;p&gt;T2，最大化每段平均值的最小值，想到二分答案。想如何 $\rm{check}$ 的过程中先想到了DP做法，就去往DP方向上想了。能找到好多种状态表示，与序列前 $i$ 位，已经分出 $j$ 段，最小的平均值为 $k$ 中的一些有关。&lt;/p&gt;
&lt;p&gt;在 2h 的时候确定了一种时间复杂度为 $\mathcal{O}(n^2k)$，空间复杂度为 $\mathcal{O}(nk)$ 的 DP，$k\leq n\leq 500$ 可以过，$40$ 分。&lt;/p&gt;
&lt;p&gt;继续看怎么优化这个DP，对着转移顺序看了一会，没发现可以用数据结构维护或者和可以利用的单调性。&lt;/p&gt;
&lt;p&gt;再想二分答案的 $\rm{check}$ 过程，整体减掉 $x$，就等价于要把序列分成 $k$ 段，每段元素和 $\geq 0$。又想DP处理这个，不会。&lt;/p&gt;
&lt;p&gt;还剩1h，去看了一眼 T3 和 T4，只有 T4 的暴力可做，有 $20$ 分，先没写，回去看 T1。&lt;/p&gt;
&lt;p&gt;又发现 $n\leq 2\times 10^4$， 感觉题意就是可以 $\mathcal{O}(n^2)$ 加小常数通过。&lt;/p&gt;
&lt;p&gt;想到对每个点对单独计算贡献，把每个点在 $m$ 次操作中处于的点集压到 &lt;code&gt;unsigned long long&lt;/code&gt; 里面存。&lt;/p&gt;
&lt;p&gt;具体地，$v_1[i]$ 拆成二进制位之后，$v_1[i][j]=1$ 当且仅当 $i$ 在第 $j$ 轮的时候处于点集 $x$，$v_2[i]$ 同理。&lt;/p&gt;
&lt;p&gt;这样可以通过简单位运算得知，每个点对之间在哪些轮次时会被翻转，用题目给到的 &lt;code&gt;__builtin_popcountll()&lt;/code&gt; 计算 popcount 即可。&lt;/p&gt;
&lt;p&gt;调完还剩 15min，去写 T4 的暴力。写完只花了不到 $10$ 分钟，但过不了样例。&lt;/p&gt;
&lt;p&gt;到最后也没看出来是因为 $i,j$ 写混，丢掉 $20$ 分。&lt;/p&gt;
&lt;p&gt;估分 $100+40+0+0$，得分 $100+50+0+0$，排名 $101/147$.&lt;/p&gt;
&lt;h1&gt;11/10 - 21noip赛前20天 day17&lt;/h1&gt;
&lt;p&gt;T1 看上去和博弈论相关，两人在过程中都按照最优策略去选，暴力都没法打。&lt;/p&gt;
&lt;p&gt;对着大样例模拟了一个多小时，发现对于每个环，只有两种情况。第一种是先手获得 $4$，后手获得 $a_i-4$；第二种是先手获得 $0$，后手获得 $a_i$，并交换先后手的顺序。&lt;/p&gt;
&lt;p&gt;如果对每个点搜索所属情况复杂度是 $\mathcal{O}(2^n)$，能拿到 $60$ 分，可以接受。&lt;/p&gt;
&lt;p&gt;但赛后发现这样是不正确的。&lt;/p&gt;
&lt;p&gt;对于选取的顺序问题，在想到上面的两种情况之前，感觉上应当升序或者降序去取，模拟大样例的时候也是按照升序去做的。&lt;/p&gt;
&lt;p&gt;写代码的时候没加 &lt;code&gt;sort&lt;/code&gt;，发现也能通过大样例，于是就忽略了顺序问题。考试最后几分钟发现了这个问题，因为其实并没有想清楚排序有没有必要，就没有加上去。&lt;/p&gt;
&lt;p&gt;由于数据点绑定 $\rm{subtask}$，$60\to 0$。&lt;/p&gt;
&lt;p&gt;写完 T1 还剩 2h，去做 T2。&lt;/p&gt;
&lt;p&gt;T2 看懂题面就去打暴力了，用并查集维护是否在同一个连通块内，每次判断需要合并的两点是否已经属于一个连通块。&lt;/p&gt;
&lt;p&gt;写了一下，能过大样例也就没想太多，直接交上去了。&lt;/p&gt;
&lt;p&gt;然而这样并不是正确的。在连通块中，是允许存在偶环的，只是不允许存在奇环。所以更准确的，应当判断合并后是否仍然是一张二分图，我暴力的方法是不正确的。&lt;/p&gt;
&lt;p&gt;于是这题也 $0$ 分了。&lt;/p&gt;
&lt;p&gt;去看 T3&amp;#x26;4 还剩 1h。&lt;/p&gt;
&lt;p&gt;想了半个小时的 T3，发现拿分都比较困难，就去写 T4 的暴力。&lt;/p&gt;
&lt;p&gt;首先有 $10$ 分暴力直接枚举，后面还有个 $20$ 分的部分，感觉需要在线段树上操作，比较麻烦，想清楚的时候只剩 10 分钟了，也没去写。&lt;/p&gt;
&lt;p&gt;赛后发现后面的 $20$ 分有不需要线段树的做法。时间复杂度虽然没有保障，但在随机数据下跑的很快，也可以拿到这 $20$ 分。&lt;/p&gt;
&lt;p&gt;估分 $60+30+0+10$，得分 $0+0+0+10$。&lt;/p&gt;
&lt;p&gt;在处理这种需要找性质的题的时候，没什么思路。经常是想到了差不多的地方，但是细节想不清楚，也证明不出。&lt;/p&gt;
&lt;p&gt;多刷 CF 可能有用？&lt;/p&gt;</content:encoded></item><item><title>【CodeForces-1089F】Fractions</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1089f</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1089f</guid><description>算法题解</description><pubDate>Sat, 30 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定正整数 $n\leq 10^9$，需要构造出不超过 $10^5$ 个真分数，满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\frac{a_1}{b_1}+\frac{a_2}{b_2}+\cdots+\frac{a_t}{b_t}+\frac{1}{n}=1$.&lt;/li&gt;
&lt;li&gt;对每个分数，$b_i&amp;#x3C;n$ 且 $b_i\mid n$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若无解，输出 $-1$，否则给出一组可行解。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;先将 $n$ 质因数分解，分下列情况讨论。&lt;/p&gt;
&lt;h3&gt;一&lt;/h3&gt;
&lt;p&gt;首先考虑特殊情况。$n = p^m$，其中 $p$ 是质数。&lt;/p&gt;
&lt;p&gt;要选一个因数作为分母，发现 $n$ 的任意一个&lt;strong&gt;因数&lt;/strong&gt; $b$，都一定是 $p^a$ 的形式。&lt;/p&gt;
&lt;p&gt;因为分母为 $1$ 时不合题意，所以在 $1$ 之外，我们任选 $n$ 的两个因子，不妨设为 $c, d$，一定有 $\gcd(c, d)\neq 1$.&lt;/p&gt;
&lt;p&gt;又因为 $\gcd(n-1, n)=1$，而 $\gcd(c, d)\neq 1$。以 $c, d$ 作为分母，一定不合法。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;当 $n=p^m$ 时，不存在合法解&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;二&lt;/h3&gt;
&lt;p&gt;对于其他情况，$n$ 一定可以被表示为&lt;strong&gt;两个互质的数的乘积&lt;/strong&gt;。一种构造方法是：&lt;/p&gt;
&lt;p&gt;将 $n$ 质因数分解，不妨 $n=p_1^{m_1}p_2^{m2}\cdots p_t^{m_t}$，令 $x=p_1^{m_1}$，$y=p_2^{m_2}\cdots p_t^{m_t}$，显然 $xy=n$ 且 $\gcd(x, y)=1$.&lt;/p&gt;
&lt;p&gt;为了方便，我们钦定 $x\leq y$。如果按照上面的方法得到的 $x&gt;y$，交换 $x, y$ 即可。&lt;/p&gt;
&lt;p&gt;当 $k=2$ 时，设题目所求的 $\frac{a_1}{b_1}=\frac{c}{x}$，$\frac{a_2}{b_2}=\frac{d}{y}$.&lt;/p&gt;
&lt;p&gt;我们需要求出一组 $(c, d)$ 满足 $\frac{c}{x}+\frac{d}{y}=1-\frac{1}{n}\Rightarrow \frac{c\cdot y+d\cdot x}{xy}=\frac{n-1}{n}$.&lt;/p&gt;
&lt;p&gt;因为 $xy=n$，这等价于&lt;strong&gt;满足：&lt;/strong&gt;
$$
c\cdot y+d\cdot x=n-1
$$
继续整理，把 $c\cdot y$ 移到右侧，得到：
$$
d=\frac{n-1-cy}{x}
$$
注意 $c, d\in \Bbb{Z}^+$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先不考虑 $d$ 的正负性&lt;/strong&gt;。由于 $x\mid n$，有：
$$
d\in \Bbb{Z}\Rightarrow x\mid(n-1-c\cdot y)\Rightarrow x\mid (1+c\cdot y)
$$
想要找到满足 $x\mid (1+c\cdot y)$ 的 $c$，考虑&lt;strong&gt;直接枚举&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注意到 $\gcd(x, y)=1$。当 $c$ 取 $1, 2, \cdots x-1$ 时，都有 $x\nmid (c\cdot y)$，所以 $c\cdot y$ 的取值构成 $\operatorname{mod} x$ 的完全剩余系。&lt;/p&gt;
&lt;p&gt;也就是说，当 $c$ 遍历 $1\sim x-1$ 时，$c\cdot y\bmod x$ 的取值两两不同，也&lt;strong&gt;恰好完全覆盖&lt;/strong&gt;了 $1\sim x-1$.&lt;/p&gt;
&lt;p&gt;因此，一定存在 $c\in[1, x-1]$ 满足 $x\mid (c\cdot y+1)$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再看 $d$ 的正负性&lt;/strong&gt;。由于 $1+c \cdot y \leq 1+(x-1) \cdot y=1+xy-y=n-(y-1) &amp;#x3C; n$，所以 $n-(1\cdot y)&gt;0$.&lt;/p&gt;
&lt;p&gt;因此，一定有 $d=\frac{n-1-cy}{x}&gt;0$.&lt;/p&gt;
&lt;h2&gt;综上&lt;/h2&gt;
&lt;p&gt;我们先按照 $x=p_1^{m_1}$，$y=p_2^{m_2}\cdots p_t^{m_t}$ 的方法找到 $(x, y)$.&lt;/p&gt;
&lt;p&gt;之后，在范围 $1\sim x-1$ 中枚举 $c$，找到满足 $x\mid(c\cdot y+1)$ 的 $c$，计算出 $d=\frac{n-1-cy}{x}$。&lt;/p&gt;
&lt;p&gt;$(\frac{c}{x}, \frac{d}{y})$ 即为满足题意的两个分数。&lt;/p&gt;
&lt;h2&gt;时间复杂度&lt;/h2&gt;
&lt;p&gt;找到 $(x, y)$ 的部分，需要对 $x$ 分解质因数，时间复杂度为 $\mathcal{O}(\sqrt{n})$.&lt;/p&gt;
&lt;p&gt;枚举 $c$ 的部分，因为我们钦定了 $x\leq y$，把 $[1, x-1]$ 看作 $x$ 个数，那么：
$$
x=\sqrt{x\cdot x}\leq \sqrt{x\cdot y}=\sqrt{n}
$$
所以总时间复杂度是 $\mathcal{O}(\sqrt{n})$ 的。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int n;
vector&amp;#x3C;int&gt; fac;
int main() {
	scanf(&quot;%d&quot;, &amp;#x26;n);
	int p1 = -1;
	for(int i = 2; i * i &amp;#x3C;= n; i++) {
		if(n % i == 0) {p1 = i; break;}
	}
	if(p1 == -1) p1 = n;
	int x = p1, y = n / p1;
	while(y % p1 == 0) y /= p1, x *= p1;
	if(y == 1) {
		cout &amp;#x3C;&amp;#x3C; &quot;NO&quot; &amp;#x3C;&amp;#x3C; endl;
		return 0;
	}
	int ansc, ansd;
	for(int c = 1; c &amp;#x3C;= x - 1; c++) {
		if((c * y + 1) % x == 0) {
			ansc = c; ansd = (n - 1 - c * y) / x;
			break;
		}
	}
	cout &amp;#x3C;&amp;#x3C; &quot;YES\n2\n&quot; &amp;#x3C;&amp;#x3C; ansc &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; x &amp;#x3C;&amp;#x3C; &quot;\n&quot; &amp;#x3C;&amp;#x3C; ansd &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; y &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces-1324F】Maximum White Subtree</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1324f</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1324f</guid><description>算法题解</description><pubDate>Wed, 27 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个节点无根树，每个节点 $u$ 有一个颜色 $a_u$，若 $a_u$ 为 $0$ 则 $u$ 是黑点，若 $a_u$ 为 $1$ 则 $u$ 是白点。&lt;/p&gt;
&lt;p&gt;对于每个节点 $u$，选出一个&lt;strong&gt;包含&lt;/strong&gt; $u$ 的连通子图，设子图中白点个数为 $cnt_1$，黑点个数为 $cnt_2$，请最大化 $cnt_1 - cnt_2$。并输出这个值。&lt;/p&gt;
&lt;p&gt;$1 \leq n \leq 2 \times 10^5$，$0 \leq a_u \leq 1$。&lt;/p&gt;
&lt;h2&gt;样例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;9
0 1 1 1 0 0 0 0 1
1 2
1 3
3 4
3 5
2 6
4 7
6 8
5 9
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;2 2 2 2 2 1 1 0 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;给每个点一个权值 $col_i$，白点 $col_i=1$，黑点 $col_i=-1$，问题转化为求包含 $u$ 的最大连通子图。&lt;/p&gt;
&lt;p&gt;考虑换根 DP。&lt;/p&gt;
&lt;p&gt;先随便定一个点作为根，不妨设为 $1$。在这棵有根树中定义状态。&lt;/p&gt;
&lt;p&gt;设 $g_u$ 表示：&lt;strong&gt;在 $i$ 的子树中且包含节点 $i$ 的&lt;/strong&gt; 最大连通块大小，转移显然：
$$
g_u = col_u+\sum_{\mathrm{father}_v=u}{\max(0, g_v)}
$$
只要子树答案 $&gt;0$，就可以直接加上来。$\mathrm{father}_v$ 表示节点 $v$ 在以 $1$ 为根时的父亲。&lt;/p&gt;
&lt;p&gt;接下来考虑换根的过程。&lt;/p&gt;
&lt;p&gt;对于节点 $u$，如果以 $u$ 为根，那么还剩下 &lt;strong&gt;$u$ 连向 $\operatorname{father}_u$ 的这棵子树&lt;/strong&gt; 没有统计进答案。&lt;/p&gt;
&lt;p&gt;容斥。可以通过以 $1$ 为根，用整棵树减去 $u$ 所在的子树得到这棵子树。&lt;/p&gt;
&lt;p&gt;设 $f_i$ 表示节点 $i$ 最终的答案。在处理节点 $u$ 时，我们希望已经计算出了 $f_{\operatorname{father}_u}$，其代表整棵树的信息。&lt;/p&gt;
&lt;p&gt;用其减去第一遍 DP 时 $u$ 对 $\operatorname{father}_u$ 的贡献，就可以得到 $\operatorname{father}_u$ 的子树对 $f_u$ 的贡献了。&lt;/p&gt;
&lt;p&gt;具体地，在有根树上，按照从根到叶子的顺序转移：
$$
f_u=g_u+\max(0, f_{\operatorname{father}_u} - \max(0, g_u))
$$
将父亲的答案消去自身影响后当成自己的子树，计算其对答案的贡献。&lt;/p&gt;
&lt;p&gt;代码实现上，不必建立两个数组。注意第一次 DP 顺序是从叶子到根，第二次是从根到叶子。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void dfs1(int x, int fa) {
	for(int v : e[x]) if(v != fa) {
		dfs1(v, x);
		if(dp[v] &gt; 0)
			dp[x] += dp[v];
	}
}
void dfs2(int x, int fa) {
	if(x != 1)
		dp[x] += max(0, dp[fa] - max(dp[x], 0));
	for(int v : e[x])
		if(v != fa) dfs2(v, x);
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P5522】[yLOI2019] 棠梨煎雪</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5522</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5522</guid><description>一道线段树练习题。</description><pubDate>Wed, 27 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/P5522&quot;&gt;P5522 [yLOI2019] 棠梨煎雪 - 洛谷 - 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $m$ 个长度为 $n$ 的 $01$ 串。存在某些位置为 &lt;code&gt;?&lt;/code&gt;，每个 &lt;code&gt;?&lt;/code&gt; 独立，既可以取 $0$，也可以是 $1$.&lt;/p&gt;
&lt;p&gt;维护 $q$ 次操作，每次操作可能为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;区间查询
&lt;ul&gt;
&lt;li&gt;查询有多少个只包含 $01$ 的串 $s$ 满足：存在一种对 &lt;code&gt;?&lt;/code&gt; 的赋值方法，使得区间内的每个 $01$ 串都能与 $s$ 完全匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单点修改
&lt;ul&gt;
&lt;li&gt;把第 $i$ 个串替换为给定的新串。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$1\leq m\leq 10^5, 1\leq q\leq 10^6, 1\leq n\leq 30$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;区间查询 $+$ 单点修改 $\Longrightarrow$ &lt;strong&gt;线段树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;主要问题在：如何处理对 $[l, r]$ 进行的查询操作。&lt;/p&gt;
&lt;p&gt;若区间中存在两个串 $s_i, s_j$，存在 $k$ 使得 $s_i[k]$ 和 $s_j[k]$ 一个是 $0$，另一个是 $1$，那么合法串的个数为 $0$。&lt;/p&gt;
&lt;p&gt;否则，只需要知道，&lt;strong&gt;有多少个 $k$ 满足：$\forall i, j\in [l, r]$，$s_i[k]=s_j[k]=$ &lt;code&gt;?&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;若有 $x$ 个满足要求的 $k$，则区间询问的答案是 $2^{x}$.&lt;/p&gt;
&lt;h3&gt;维护信息&lt;/h3&gt;
&lt;p&gt;考虑对 $m$ 个字符串建立线段树，&lt;strong&gt;以编号为下标&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;发现 $1\leq n\leq 30$，可以用 &lt;code&gt;int&lt;/code&gt; 类型存储&lt;strong&gt;二进制压缩&lt;/strong&gt;后的的状态。&lt;/p&gt;
&lt;p&gt;考虑用线段树：&lt;strong&gt;$v_0$ 记录必须为 $0$ 的位置，$v_1$ 记录必须为 $1$ 的位置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体地，设线段树的 $i$ 号节点代表的区间为 $[l_i, r_i]$，维护下列信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;十进制数 $v_0$：在 $v_0$ 的&lt;strong&gt;二进制&lt;/strong&gt;表示下，第 $j$ 位为 $1$ 当且仅当字符串的第 $j$ 为&lt;strong&gt;被确定为了 $0$&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;十进制数 $v_1$：在 $v_1$ 的二进制表示下，第 $j$ 位为 $1$ 当且仅当字符串的第 $j$ 为被确定为了 $1$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据题目性质，线段树&lt;strong&gt;对 $v_0$ 和 $v_1$ 维护区间&lt;code&gt;按位或&lt;/code&gt;和&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也就是说，对于 $\rm{push_up}$ 操作，把左右两端&lt;code&gt;按位或&lt;/code&gt;即可，即：
$$
x.v_0=\operatorname{lson_x}.v_0\mid \operatorname{rson_x}.v_0
$$&lt;/p&gt;
&lt;p&gt;$$
x.v_1=\operatorname{lson_x}.v_1\mid \operatorname{rson_x}.v_1
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void push_up(int id) {
    c[id].v0 = c[ls(id)].v0 | c[rs(id)].v0;
    c[id].v1 = c[ls(id)].v1 | c[rs(id)].v1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;询问&lt;/h3&gt;
&lt;p&gt;和一般的区间查询一样，但每次要包含两个返回值，与 $v_0, v_1$ 的含义相同，记录当前查询区间的信息。&lt;/p&gt;
&lt;p&gt;还是可以用&lt;code&gt;按位或&lt;/code&gt;合并左右两个区间。&lt;/p&gt;
&lt;p&gt;当遇到左右区间产生矛盾时，直接 $v_0\leftarrow (-1)$ 来代表此次询问无解，向上递归时遇到 $-1$ 就不用合并区间了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断无解：$v_0; &amp;#x26; ; v_1\neq 0$ 时无解&lt;/strong&gt;。此时存在一个 $k$ 满足 $s[k]$ 必须为 $0$ 且 $s[k]$ 必须为 $1$，矛盾。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;pair&amp;#x3C;int, int&gt; query(int id, int l, int r, int X, int Y) {
    if(X &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= Y) {
        if(c[id].v0 &amp;#x26; c[id].v1) return make_pair(-1, -1);
        else return make_pair(c[id].v0, c[id].v1);
    }
    int mid = (l + r) &gt;&gt; 1;
    pair&amp;#x3C;int, int&gt; t1 = make_pair(0, 0), t2 = make_pair(0, 0), ret = make_pair(0, 0);
    if(X &amp;#x3C;= mid) t1 = query(ls(id), l, mid, X, Y);
    if(Y &gt; mid) t2 = query(rs(id), mid + 1, r, X, Y);
    if(t1.first == -1 || t2.first == -1) return make_pair(-1, -1);
    ret.first = t1.first | t2.first; ret.second = t1.second | t2.second;
    if(ret.first &amp;#x26; ret.second) return make_pair(-1, -1);
    else return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改&lt;/h3&gt;
&lt;p&gt;就是&lt;strong&gt;普通的单点修改&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void update(int id, int l, int r, int X, int w0, int w1) {
    if(l == X &amp;#x26;&amp;#x26; r == X) {
        c[id].v0 = w0; c[id].v1 = w1;
        return ;
    }
    int mid = (l + r) &gt;&gt; 1;
    if(X &amp;#x3C;= mid) update(ls(id), l, mid, X, w0, w1);
    else update(rs(id), mid + 1, r, X, w0, w1);
    push_up(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXM = 1e5 + 10;
struct Point{
    int v0; //v0[i]=1 表示：二进制下的第i位确定为 0
    int v1; //v1[i]=1 表示：二进制下的第i位确定为 1
} c[MAXM &amp;#x3C;&amp;#x3C; 2];
int n, m, q;
int v0[MAXM], v1[MAXM];
int lc[MAXM &amp;#x3C;&amp;#x3C; 2], rc[MAXM &amp;#x3C;&amp;#x3C; 2];
namespace Segment_Tree{
    #define ls(x) (x &amp;#x3C;&amp;#x3C; 1)
    #define rs(x) (x &amp;#x3C;&amp;#x3C; 1 | 1)
    void push_up(int id) {
        c[id].v0 = c[ls(id)].v0 | c[rs(id)].v0;
        c[id].v1 = c[ls(id)].v1 | c[rs(id)].v1;
    }
    void build(int id, int l, int r) {
        lc[id] = l; rc[id] = r;
        if(l == r) {
            c[id] = (Point){v0[l], v1[l]};
            return ;
        }
        int mid = (l + r) &gt;&gt; 1;
        build(ls(id), l, mid);
        build(rs(id), mid + 1, r);
        push_up(id);
    }
    void update(int id, int l, int r, int X, int w0, int w1) {
        if(l == X &amp;#x26;&amp;#x26; r == X) {
            c[id].v0 = w0; c[id].v1 = w1;
            return ;
        }
        int mid = (l + r) &gt;&gt; 1;
        if(X &amp;#x3C;= mid) update(ls(id), l, mid, X, w0, w1);
        else update(rs(id), mid + 1, r, X, w0, w1);
        push_up(id);
    }
    pair&amp;#x3C;int, int&gt; query(int id, int l, int r, int X, int Y) {
        if(X &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= Y) {
            if(c[id].v0 &amp;#x26; c[id].v1) return make_pair(-1, -1);
            else return make_pair(c[id].v0, c[id].v1);
        }
        int mid = (l + r) &gt;&gt; 1;
        pair&amp;#x3C;int, int&gt; t1 = make_pair(0, 0), t2 = make_pair(0, 0), ret = make_pair(0, 0);
        if(X &amp;#x3C;= mid) t1 = query(ls(id), l, mid, X, Y);
        if(Y &gt; mid) t2 = query(rs(id), mid + 1, r, X, Y);
        if(t1.first == -1 || t2.first == -1) return make_pair(-1, -1);
        ret.first = t1.first | t2.first; ret.second = t1.second | t2.second;
        if(ret.first &amp;#x26; ret.second) return make_pair(-1, -1);
        else return ret;
    }
};
using namespace Segment_Tree;
string in;
int main() {
    scanf(&quot;%d%d%d&quot;, &amp;#x26;n, &amp;#x26;m, &amp;#x26;q);
    for(int i = 1; i &amp;#x3C;= m; i++) {
        cin &gt;&gt; in;
        for(int j = 0; j &amp;#x3C; n; j++) {
            v0[i] &amp;#x3C;&amp;#x3C;= 1; v1[i] &amp;#x3C;&amp;#x3C;= 1;
            if(in[j] == &apos;0&apos;) v0[i] |= 1;
            if(in[j] == &apos;1&apos;) v1[i] |= 1;
        }
    }
    build(1, 1, m);
    int ans = 0;
    while(q--) {
        int opt; scanf(&quot;%d&quot;, &amp;#x26;opt);
        if(opt == 0) {
            int l, r; scanf(&quot;%d%d&quot;, &amp;#x26;l, &amp;#x26;r);
            pair&amp;#x3C;int, int&gt; ret = query(1, 1, m, l, r); 
            if(ret.first == -1) continue;
            int tmp = ret.first | ret.second, cnt = 0;
            for(int i = 0; i &amp;#x3C; n; i++) if((tmp &amp;#x26; (1 &amp;#x3C;&amp;#x3C; i)) == 0) cnt++;
            ans ^= (1 &amp;#x3C;&amp;#x3C; cnt); 
        } else {
            int pos, w0 = 0, w1 = 0; cin &gt;&gt; pos &gt;&gt; in;
            for(int i = 0; i &amp;#x3C; n; i++) {
                w0 &amp;#x3C;&amp;#x3C;= 1; w1 &amp;#x3C;&amp;#x3C;= 1;
                if(in[i] == &apos;0&apos;) w0 |= 1;
                if(in[i] == &apos;1&apos;) w1 |= 1;
            }
            update(1, 1, m, pos, w0, w1);
        }
    }
    cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P5994】[PA2014]Kuglarz</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5994</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5994</guid><description>算法题解</description><pubDate>Wed, 27 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;有 $n$ 个杯子排成一行，编号为 $1,2,…,n$，其中某些杯子底下藏有一个小球。&lt;/p&gt;
&lt;p&gt;对于任意的 $i\leq j$，题目给定 $c_{i, j}$，表示花费 $c_{ij}$ 元，你就可以知道 $i,i+1,…,j$ 底下藏有球的总数的奇偶性。&lt;/p&gt;
&lt;p&gt;求至少需要花费多少元，才能保证猜出哪些杯子底下藏着球？&lt;/p&gt;
&lt;p&gt;$1\le n\le 2\times 10^3$，$1\le c_{ij}\le 10^9$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;不能区间DP，时间复杂度都不对。&lt;/p&gt;
&lt;p&gt;猜出每个杯子里是否有小球 $\Leftrightarrow$ 知道每个 $[i, i]$ 的奇偶性 $\Leftrightarrow$ 知道每个 $[1, i]$ 的奇偶性。&lt;/p&gt;
&lt;p&gt;因为 $i$ 的奇偶性可以通过 $[1, i]-[1, i-1]$ 得到。&lt;/p&gt;
&lt;p&gt;花费 $c_{i, j}$ 的代价查询 $[i, j]$ 的奇偶性之后，若我们知道 $[1, i-1]$ 的奇偶性，那么 $[1, j]$ 可以直接计算出。&lt;/p&gt;
&lt;p&gt;这并不完全正确，$i=1$ 时，区间 $[1, 0]$ 是不存在的，所以我们把前缀的左端点从 $1$ 改成 $0$。&lt;/p&gt;
&lt;p&gt;花费 $c_{i, j}$ 的代价查询 $[i, j]$ 的奇偶性之后，若我们知道 $[0, i-1]$ 的奇偶性，那么 $[0, j]$ 可以直接计算出。&lt;/p&gt;
&lt;p&gt;我们在 $i-1$ 和 $j$ 之间连一条权值为 $c_{i, j}$ 的边，表示：在 $[0, i-1]$ 和 $[0, j]$ 二者之中，只要知道一个的奇偶性，花费 $c_{i, j}$ 就可以知道另一个。&lt;/p&gt;
&lt;p&gt;那么在这样的图上，$[l, r]$ 连通等价于：已知区间 $[0, l-1]$ 和 $[0, r]$ 的奇偶性。&lt;/p&gt;
&lt;p&gt;要选代价一组代价和最小的边，使图连通。&lt;/p&gt;
&lt;p&gt;求 $n+1$ 个点的最小生成树即可。&lt;/p&gt;
&lt;p&gt;$\rm{Kruscal}$，时间复杂度 $\mathcal{O}(n^2\log n)$.&lt;/p&gt;</content:encoded></item><item><title>欧拉图</title><link>https://www.tonyyin0418.com/blog/oi-notes/euler_path</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/euler_path</guid><description>欧拉路径、欧拉回路的定义、判定与寻找。</description><pubDate>Thu, 21 Oct 2021 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;/p&gt;
&lt;p&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;/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;h2&gt;有向图欧拉路径&lt;/h2&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;恰好存在一个点 $S$，其&lt;strong&gt;出度比入度多一&lt;/strong&gt;；恰好存在一个点 $T$，其&lt;strong&gt;入度比出度多一&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;除 $S$ 和 $T$ 外，&lt;strong&gt;其他点的出度等于入度&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;无向图欧拉回路&lt;/h2&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;h2&gt;无向图欧拉路径&lt;/h2&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;恰有 $0$ 个或 $2$ 个度数为奇数的顶点。（$0$ 个时有欧拉回路，否则 $2$ 个奇度点为起点和终点）&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;寻找&lt;/h1&gt;
&lt;p&gt;利用 &lt;strong&gt;Hierholzer&lt;/strong&gt; 算法。&lt;/p&gt;
&lt;h2&gt;Hierholzer 算法&lt;/h2&gt;
&lt;p&gt;首先，用上面的判定方法，判断是否存在欧拉路径或欧拉回路，并确定起点。&lt;/p&gt;
&lt;p&gt;之后，从起点 $S$ 开始进行 &lt;code&gt;DFS&lt;/code&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;$$
\begin{array}{ll}
1 &amp;#x26;  \textbf{Input. } \text{The edges of the graph } e , \text{ where each element in } e \text{ is } (u, v) \
2 &amp;#x26;  \textbf{Output. } \text{The vertex of the Euler Road of the input graph}.\
3 &amp;#x26;  \textbf{Method. } \
4 &amp;#x26;  \textbf{Function } \text{Hierholzer } (u) \
5 &amp;#x26;  \qquad \textbf{for } \text{each Edge} e \text{ Begin with } u\
6 &amp;#x26;  \qquad\qquad \textbf{delete } e \text{ from E}\
7 &amp;#x26;  \qquad\qquad \text{Hierholzer }(v) \
8 &amp;#x26;  \qquad\textbf{Insert } v  \text{ into stack}\
9 &amp;#x26;  \textbf{Endfunction}\
10&amp;#x26; \textbf{Print } \text{stack in Reverse order}.
\end{array}
$$&lt;/p&gt;
&lt;h1&gt;P7771 【模板】欧拉路径&lt;/h1&gt;
&lt;p&gt;给定有向图，保证其弱联通，求字典序最小的欧拉路径。&lt;/p&gt;
&lt;p&gt;对每个点的出边按字典序排序，跑 $\textbf{Hierholzer}$ 即可。&lt;/p&gt;
&lt;p&gt;代码用优先队列实现了排序这一过程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int MAXN = 1e5 + 10, MAXM = 2e5 + 10;
int n, m;
priority_queue&amp;#x3C;int, vector&amp;#x3C;int&gt;, greater&amp;#x3C;int&gt; &gt; e[MAXN];
int oud[MAXN], ind[MAXN];
vector&amp;#x3C;int&gt; path;
void Hierholzer(int x) {
	while(!e[x].empty()) {
		int v = e[x].top(); e[x].pop();
		Hierholzer(v);
	}
	path.push_back(x);
}
int main() {
	n = read(); m = read();
	for(int i = 1; i &amp;#x3C;= m; i++) {
		int u = read(), v = read();
		e[u].push(v);
		ind[v]++; oud[u]++;
	}
	int ok = 1, S = 0, T = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		if(oud[i] - ind[i] == 1) {
			if(S) ok = 0;
			else S = i;
		} else if(ind[i] - oud[i] == 1) {
			if(T) ok = 0;
			else T = i;
		} else if(ind[i] != oud[i]) ok = 0;
	}
	if(!ok) printf(&quot;No\n&quot;);
	else if((S == 0 &amp;#x26;&amp;#x26; T != 0) || (S != 0 &amp;#x26;&amp;#x26; T == 0)) printf(&quot;NO\n&quot;);
	else {
		if(S == 0) S = 1; //此时S==0&amp;#x26;&amp;#x26;T==0，有欧拉回路，随便定起点
		Hierholzer(S);
		for(int i = path.size() - 1; i &gt;= 0; i--) printf(&quot;%d &quot;, path[i]);
		putchar(&apos;\n&apos;);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3426】[POI2005]SZA-Template</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3426</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3426</guid><description>算法题解</description><pubDate>Tue, 19 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.com.cn/problem/P3426&quot;&gt;P3426 [POI2005]SZA-Template - 洛谷 - 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;设 $\operatorname{dp}[i]$ 表示前缀 $[1, i]$ 的答案，设 $\operatorname{lst}[i]$ 表示以前缀 $[1, i]$ 为印章，最远能覆盖到的位置。&lt;/p&gt;
&lt;p&gt;容易发现，前缀 $[1, i]$ 一定能被自己覆盖，因此 $\operatorname{dp}[i]$ 最大为 $i$。&lt;/p&gt;
&lt;p&gt;其何时能被更短的段覆盖?&lt;/p&gt;
&lt;p&gt;考虑其 $\rm{border}$，我们以覆盖其 $\rm{border}$ 相同的方式，也就是以 $\operatorname{dp}[\operatorname{border}[i]]$ 为印章长度，尝试覆盖 $[1, i]$。&lt;/p&gt;
&lt;p&gt;如果可行，那么 $\operatorname{dp}[i]=\operatorname{dp}[\operatorname{border}[i]]$。&lt;/p&gt;
&lt;p&gt;判断可行？&lt;/p&gt;
&lt;p&gt;可行当且仅当：$\operatorname{lst}[\operatorname{dp}[\operatorname{border}[i]]] &gt;= i - \operatorname{border}[i]$.&lt;/p&gt;
&lt;p&gt;意思就是，以 $\operatorname{dp}[\operatorname{border}[i]]$ 为印章长度，至少要可以覆盖到 $i - \operatorname{border}[i]$。&lt;/p&gt;
&lt;p&gt;$[i - \operatorname{border}[i], i]$ 这段与 $[1, \operatorname{border}[i]]$ 完全相同，一定能被覆盖，所以只需 $[i-\operatorname{border}[i]]$ 之前能被覆盖。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXLEN  = 5e5 + 10;
char s[MAXLEN];
int len, nxt[MAXLEN];
void get_border() {
    nxt[1] = 0;
    for(int i = 2; i &amp;#x3C;= len; i++) {
        int match = nxt[i - 1];
        while(s[i] != s[match + 1] &amp;#x26;&amp;#x26; match != 0) match = nxt[match];
        if(s[i] == s[match + 1]) match++;
        nxt[i] = match;
    }
}
int dp[MAXLEN]; //dp[i]表示前缀[1, i]的答案
int lst[MAXLEN]; //lst[i]表示以前缀[1, i]为印章，最远能覆盖到的位置
int main() {
    scanf(&quot;%s&quot;, (s + 1)); len = strlen(s + 1);
    get_border();
    dp[1] = 1; lst[1] = 1;
    for(int i = 2; i &amp;#x3C;= len; i++) {
        dp[i] = i;
        if(lst[dp[nxt[i]]] &gt;= i - nxt[i]) dp[i] = dp[nxt[i]];
        lst[dp[i]] = i;
    }
    cout &amp;#x3C;&amp;#x3C; dp[len] &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>高斯消元</title><link>https://www.tonyyin0418.com/blog/oi-notes/gauss-elimination</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/gauss-elimination</guid><description>2021.10.16，CSP 赛前专题分享，文字版讲稿</description><pubDate>Sat, 16 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;高斯消元法&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;高斯消元法（Gauss-Jordan elimination），可以用于解决：线性方程组求解、行列式求值、矩阵求逆等问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;给定一个线性方程组，要求解。&lt;/p&gt;
&lt;h2&gt;一些定义&lt;/h2&gt;
&lt;p&gt;变量 $=$ 元 $=$ 变元 $=$ 未知数。&lt;/p&gt;
&lt;p&gt;对于一个 $m$ 元线性方程组，已知系数 $a_{ij}$​，未知量 $x_i$​，即：
$$
\left{
\begin{array}{}
a_{1,1}x_1+a_{1,2}x_2+\cdots+a_{1,m}x_m=c_1 &amp;#x26;(1)\
a_{2,1}x_1+a_{2,2}x_2+\cdots+a_{2,m}x_m=c_2 &amp;#x26;(2)\
\cdots\
a_{n,1}x_1+a_{n,2}x_2+\cdots+a_{n,m}x_m=c_n &amp;#x26;(n)
\end{array}
\right.
$$
将简记为一个 $n$ 行 $m$ 列的矩阵：
$$
\left[\begin{matrix}
a_{1,1} &amp;#x26; a_{1,2} &amp;#x26; \cdots &amp;#x26; a_{1,m} \
a_{2,1} &amp;#x26; a_{2,2} &amp;#x26; \cdots &amp;#x26; a_{2,m} \
\cdots &amp;#x26; \cdots &amp;#x26; \cdots &amp;#x26; \cdots \
a_{n,1} &amp;#x26; a_{n,2} &amp;#x26; \cdots &amp;#x26; a_{n,m}
\end{matrix} \right]
$$
我们称这个矩阵为 &lt;strong&gt;系数矩阵&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在系数矩阵右侧，&lt;strong&gt;加一列常数项&lt;/strong&gt;，称其为 &lt;strong&gt;增广矩阵&lt;/strong&gt;：
$$
\left[\begin{matrix}
a_{1,1} &amp;#x26; a_{1,2} &amp;#x26; \cdots &amp;#x26; a_{1,m} \
a_{2,1} &amp;#x26; a_{2,2} &amp;#x26; \cdots &amp;#x26; a_{2,m} \
\cdots &amp;#x26; \cdots &amp;#x26; \cdots &amp;#x26; \cdots \
a_{n,1} &amp;#x26; a_{n,2} &amp;#x26; \cdots &amp;#x26; a_{n,m}
\end{matrix} \middle|
\begin{matrix}
c_1 \
c_2 \
\cdots\
c_n
\end{matrix} \right]
$$
竖线 $\mid$ 没有其他含义，只是为了区分等式左右两侧。两侧同属一个矩阵。&lt;/p&gt;
&lt;p&gt;为了方便表示，&lt;strong&gt;后面出现的 $c_i$ 可能会用 $a_{i, m+1}$ 代替&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;简单情况&lt;/h2&gt;
&lt;p&gt;从&lt;strong&gt;简单情况&lt;/strong&gt;开始考虑，假设恰好 $n=m$，且每个方程都有意义，方程组形如：
$$
\left{
\begin{aligned}
&amp;#x26;a_{1,1}x_1&amp;#x26;+ &amp;#x26;a_{1,2}x_2&amp;#x26;+\cdots+ &amp;#x26;a_{1, n-1}x_{n-1} &amp;#x26;+ &amp;#x26;a_{1,n}x_n=c_1 &amp;#x26;(1)\
&amp;#x26;0\cdot x_1&amp;#x26;+ &amp;#x26;a_{2,2}x_2&amp;#x26;+\cdots+ &amp;#x26;a_{2, n-1}x_{n-1} &amp;#x26;+ &amp;#x26;a_{2,n}x_n=c_2 &amp;#x26;(2)\
&amp;#x26;0\cdot x_1&amp;#x26;+ &amp;#x26;0\cdot x_2&amp;#x26;+\cdots+ &amp;#x26;a_{3, n-1}x_{n-1} &amp;#x26;+ &amp;#x26;a_{3,n}x_n=c_2 &amp;#x26;(3)\
&amp;#x26;&amp;#x26;&amp;#x26;&amp;#x26;+\cdots+\
&amp;#x26;0\cdot x_1&amp;#x26;+ &amp;#x26;0\cdot x_2&amp;#x26;+\cdots+ &amp;#x26;0\cdot x_{n-1} &amp;#x26;+ &amp;#x26;a_{n,n}x_n=c_n &amp;#x26;(n)
\end{array}
\right.
$$
矩阵为：
$$
\left[\begin{matrix}
a_{1,1} &amp;#x26; a_{1,2} &amp;#x26; \cdots &amp;#x26; a_{1,n-1} &amp;#x26; a_{1,n} \
0 &amp;#x26; a_{2,2} &amp;#x26; \cdots &amp;#x26; a_{2,n-1} &amp;#x26; a_{2,n} \
0 &amp;#x26; 0 &amp;#x26; \cdots &amp;#x26; a_{3,n-1} &amp;#x26; a_{3,n} \
\cdots &amp;#x26; \cdots &amp;#x26; \cdots &amp;#x26; \cdots &amp;#x26; \cdots \
0 &amp;#x26; 0 &amp;#x26; \cdots &amp;#x26; 0 &amp;#x26; a_{n,n}
\end{matrix} \middle|
\begin{matrix}
a_{1,n+1} \
a_{2,n+1} \
a_{3,n+1} \
\cdots\
a_{n,n+1}
\end{matrix} \right]
$$
矩阵的左下角都是 $0$.&lt;/p&gt;
&lt;p&gt;对于这种倒三角矩阵，最后一行代表的方程为：$a_{nm}\cdot x_m=a_{n,m+1}$，可以直接求出 $x_m=\frac{a_{n, m+1}}{a_{nm}}$.&lt;/p&gt;
&lt;p&gt;之后，我们再将 $x_m$ 代入倒数第二行的方程：$a_{n-1,m-1}\cdot x_{m-1}+a_{n-1,m}\cdot x_m=a_{n-1, m+1}$，求出 $x_{m-1}$.&lt;/p&gt;
&lt;p&gt;以此类推，就可以求出所有的未知量了。&lt;/p&gt;
&lt;p&gt;考虑如何&lt;strong&gt;将原矩阵变换为倒三角矩阵&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这就和一般的消元法解方程一样，要让第 $x_i$ 个未知数只存在于前 $i$ 个方程（其它方程中 $x_i$ 系数均为 $0$）.&lt;/p&gt;
&lt;p&gt;枚举列，也就是枚举未知数。当我们枚举到第 $c$ 列的时候，我们要让 $\sum_{i=c+1}^{n}[a_{i, c}\neq0]=0$.&lt;/p&gt;
&lt;p&gt;所以对于第 $i\in [c+1, n]$ 行，整行一起变换，使 $a_{i,c}=0$：
$$
\forall j\in [1,m+1],\quad a_{i, j}\leftarrow a_{i, j}-a_{c, j}\cdot\frac{a_{i, c}}{a_{c, c}}
$$
由于 $\forall j\in[1, c-1]$, $a_{c, j}=0$，所以上式与 $j\in[c, m+1]$ 是等价的。&lt;/p&gt;
&lt;p&gt;之后进行回带，把所有无关项移到等式右边。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double A[MAXN][MAXN], ans[MAXN];
void Gauss() {
	for(int c = 1; c &amp;#x3C;= n; c++) {
		for(int i = c + 1; i &amp;#x3C;= n; i++) {
			double t = A[i][c] / A[c][c];
			for(int j = c + 1; j &amp;#x3C;= n + 1; j++) {
				A[i][j] -= A[c][j] * t;
			}
		}
	}
    for(int i = n; i &gt;= 1; i--) {
        for(int j = i + 1; j &amp;#x3C;= n; j++) a[i][n + 1] -= a[i][j] * ans[j];
        ans[i] = a[i][n + 1] / a[i][i];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提高精度&lt;/h2&gt;
&lt;p&gt;在消元过程中，对于第 $i$ 个主元，我们默认选择第 $i$ 行的 $a_{i, i}$ 作为保留的元素。&lt;/p&gt;
&lt;p&gt;为了减小误差，我们在第 $i$ 列的 $a_{i, j}(j\geq i)$ 中选一个最大的数作为保留的元素，之后交换第 $i$ 行和第 $j$ 行。&lt;/p&gt;
&lt;p&gt;显然，这样有助于精度的控制。&lt;/p&gt;
&lt;h2&gt;扩大适用范围&lt;/h2&gt;
&lt;p&gt;前面的方法基于 $n=m$，且方程组存在唯一解。然而，方程个数与未知数不一定相等，解的个数也不一定唯一，下面给出&lt;strong&gt;更普适的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;判断解的个数&lt;/h4&gt;
&lt;p&gt;容易发现，由 $n$ 个方程构成的 $m$ 元线性方程组，解的个数只可能为：$0, 1, \infty$.&lt;/p&gt;
&lt;p&gt;为了方便判断，我们引入&lt;strong&gt;自由元&lt;/strong&gt;的定义：取值不确定的变元。&lt;/p&gt;
&lt;p&gt;当我们枚举到第 $i$ 个变量，且前 $i$ 个变量都不是自由元时，如果第 $i\sim n$ 行中&lt;strong&gt;这个变量的系数均为 $0$&lt;/strong&gt;，那么第 $i$ 个变量是自由元。
$$
\left[\begin{matrix}
a_{1,1} &amp;#x26; a_{1,2} &amp;#x26; a_{1, 3}      &amp;#x26; a_{1,4} &amp;#x26; a_{1,5} \
0       &amp;#x26; a_{2,2} &amp;#x26; a_{2, 3}      &amp;#x26; a_{2,4} &amp;#x26; a_{2,5} \
0       &amp;#x26; 0       &amp;#x26; \color{red}{0}&amp;#x26; a_{3,4} &amp;#x26; a_{3,5} \
0       &amp;#x26; 0       &amp;#x26; 0             &amp;#x26; a_{4,4} &amp;#x26; a_{4,5} \
0       &amp;#x26; 0       &amp;#x26; 0             &amp;#x26; 0       &amp;#x26; a_{5,5}
\end{matrix} \middle |
\begin{matrix}
c_{1} \
c_{2} \
c_{3} \
c_{4} \
c_{5}
\end{matrix} \right]
$$
上面的例子中，$x_3$ 即为自由元。可以发现，当 $x_4, x_5$ 被确定之后，第三行的方程可以记为：
$$
0\cdot x_3=c_3-x_4\cdot a_{3, 4}-x_5\cdot a_{3, 5}=C
$$
若 $C=0$，则 $x_3$ 可以在 $\Bbb{R}$ 中任取。&lt;/p&gt;
&lt;p&gt;若 $C\neq 0$，则 $x_3$ 一定无解，方程组也一定无解。&lt;/p&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;h4&gt;解决 $n\neq m$ 的情况&lt;/h4&gt;
&lt;p&gt;在枚举变量的过程当中，会发现有些变量是自由元。这时我们&lt;strong&gt;应当跳过这一列，并且保持行不变&lt;/strong&gt;，处理下一个变元。&lt;/p&gt;
&lt;p&gt;因此我们用指针 $\operatorname{row}$ 和 $\operatorname{col}$ 表示当前所在的行和列，最后 $\operatorname{row}$ 的位置即为&lt;strong&gt;非自由元的个数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于遇到所有自由元时，我们都保持行不变，所以可以理解为：我们把所有自由元都挪到了最后面没有被 $\operatorname{row}$ 遍历到的几行当中去。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这可以结合上面 $5\times 5$ 的例子进行理解。&lt;/p&gt;
&lt;p&gt;在遇到自由元 $x_3$ 后，跳过第 $3$ 列，保持 $\operatorname{row}$ 不变。所以第 $3$ 行保留的主元是 $x_4$，同理第 $4$ 行保留的主元是 $x_5$.&lt;/p&gt;
&lt;p&gt;所以第 $5$ 行即代表第 $x_3$，第 $5$ 行的方程为：$0\times x_3=c_5$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;下面代码会将所有自由元保留到最后的连续几行。&lt;/p&gt;
&lt;p&gt;因此，要判断解的情况，只需要去判断第 $\operatorname{row+1}\sim n$ 行的系数 $c$ 是否为 $0$ 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//高斯消元法，返回-1为无解，0为无穷解，1为唯一解
double A[MAXN][MAXN], ans[MAXN];
int Gauss() {
	int row = 1, max_row;
	for(int col = 1; col &amp;#x3C;= m; col++) {
		max_row = row;
		for(int i = row + 1; i &amp;#x3C;= n; i++) { //提高精度 找到一列中值最大的
			if(fabs(A[i][col]) &gt; fabs(A[max_row][col])) max_row = i;
		}
		if(fabs(A[max_row][col]) &amp;#x3C; eps)
			continue; //如果col是自由元，那么跳过这一列，但行不变
		if(max_row != row) { //swap(当前行，最大值所在的行)
			for(int i = 1; i &amp;#x3C;= m + 1; i++) swap(A[row][i], A[max_row][i]);
		}
		for(int i = row + 1; i &amp;#x3C;= n; i++) {
			double t = A[i][col] / A[row][col];
			for(int j = 1; j &amp;#x3C;= m + 1; j++) {
				A[i][j] -= A[row][j] * t;
			}
		}
		row++;
	}
	row--;
	if(row &amp;#x3C; n) {
		for(int i = row + 1; i &amp;#x3C;= n; i++) {
			//如果左边所有系数都是0，等号右边不是0，则无解
			if(abs(A[i][m + 1]) &gt; eps) return -1;
		} //否则有无数解
		return 0;
	}
	for(int i = m; i &gt;= 1; i--) { //回代
		for(int j = i + 1; j &amp;#x3C;= m; j++) A[i][m + 1] -= A[i][j] * ans[j];
		ans[i] = A[i][m + 1] / A[i][i];
	}
	return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;高斯-约旦消元法&lt;/h1&gt;
&lt;blockquote&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;/blockquote&gt;
&lt;h2&gt;算法思想&lt;/h2&gt;
&lt;p&gt;构造方程组矩阵的方法同上，化简后的矩阵形如：
$$
\left[\begin{matrix}
a_{1,1} &amp;#x26;0        &amp;#x26; 0      &amp;#x26;\cdots &amp;#x26; 0     &amp;#x26; 0 \
0       &amp;#x26; a_{2,2} &amp;#x26; 0      &amp;#x26;\cdots &amp;#x26; 0     &amp;#x26; 0 \
0       &amp;#x26; 0       &amp;#x26;a_{3,3} &amp;#x26;\cdots &amp;#x26; 0     &amp;#x26; 0 \
\cdots  &amp;#x26;\cdots   &amp;#x26;\cdots  &amp;#x26;\cdots &amp;#x26;\cdots &amp;#x26; \cdots \
0       &amp;#x26; 0       &amp;#x26;0       &amp;#x26;\cdots &amp;#x26; 0     &amp;#x26; a_{n,m}
\end{matrix} \middle|
\begin{matrix}
c_{1} \
c_{2} \
c_{3} \
\cdots\
c_{n}
\end{matrix} \right]
$$
&lt;strong&gt;只保留主对角线上的元素&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;显然，这样就省去了回代的步骤，直接 $a_{i, i}\cdot x_i=c_i$ 就可以解出所有未知数了。&lt;/p&gt;
&lt;h2&gt;算法实现&lt;/h2&gt;
&lt;p&gt;在上面的&lt;a href=&quot;#%E9%AB%98%E6%96%AF%E6%B6%88%E5%85%83%E6%B3%95&quot;&gt;高斯消元法&lt;/a&gt;中，有这样一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当我们枚举到第 $c$ 列的时候，我们要让 $\sum_{i=c+1}^{n}[a_{i, c}\neq0]=0$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要想让最终的矩阵只保留主对角线上的元素，我们只需要把这句话改为：&lt;/p&gt;
&lt;p&gt;当我们枚举到第 $c$ 列的时候，我们要让 $\sum_{i\neq c}[a_{i, c}\neq0]=0$，也就是&lt;strong&gt;直接消掉其他所有行&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;double A[MAXN][MAXN], ans[MAXN];
void Gauss_Jordan() {
	for(int c = 1; c &amp;#x3C;= n; c++) {
		for(int i = c + 1; i &amp;#x3C;= n; i++) {
			double t = A[i][c] / A[c][c];
			for(int j = c + 1; j &amp;#x3C;= n + 1; j++) {
				A[i][j] -= A[c][j] * t;
			}
		}
	}
}
&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;两种算法的区别只在第 $\rm{15\sim16}$ 行、第 $33$ 行这两处。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//高斯-约旦消元法，返回-1为无解，0为无穷解，1为唯一解
double A[MAXN][MAXN], ans[MAXN];
int Gauss_Jordan() {
	int row = 1, max_row;
	for(int col = 1; col &amp;#x3C;= m; col++) {
		max_row = row;
		for(int i = row + 1; i &amp;#x3C;= n; i++) {
			if(fabs(A[i][col]) &gt; fabs(A[max_row][col])) max_row = i;
		}
		if(fabs(A[max_row][col]) &amp;#x3C; eps)
			continue; //如果col是自由元，那么跳过这一列，但行不变
		if(max_row != row) { //swap(当前行，最大值所在的行)
			for(int i = 1; i &amp;#x3C;= n + 1; i++) swap(A[row][i], A[max_row][i]);
		}
		for(int i = 1; i &amp;#x3C;= n; i++) {
			if(i == row) continue; //每列只保留第row行的元素
			double t = A[i][col] / A[row][col];
			for(int j = 1; j &amp;#x3C;= m + 1; j++) {
				A[i][j] -= A[row][j] * t;
			}
		}
		row++;
	}
	row--;
	if(row &amp;#x3C; n) {
		for(int i = row + 1; i &amp;#x3C;= n; i++) {
			//如果左边所有系数都是0，等号右边不是0，则无解
			if(fabs(A[i][n + 1]) &gt; eps) return -1;
		} //否则有无数解
		return 0;
	}
	for(int i = 1; i &amp;#x3C;= m; i++) {
		ans[i] = A[i][m + 1] / A[i][i];
	}
	return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;解异或方程组&lt;/h1&gt;
&lt;p&gt;对于异或方程组，高斯消元法同样适用。&lt;/p&gt;
&lt;p&gt;在实数方程组中，我们消掉第 $v$ 个变元的方法是：用第 $a$ 行减去第 $b$ 行的 $k$ 倍，使第 $a$ 行第 $v$ 列变为 $0$。&lt;/p&gt;
&lt;p&gt;在异或方程组中，若第 $a$ 行第 $v$ 列的值为 $1$，就&lt;strong&gt;将第 $a$ 行异或上第 $b$ 行&lt;/strong&gt;。（按列分别异或）&lt;/p&gt;
&lt;p&gt;由于 $x\in{0, 1}$，所以 $(a\cdot x)\oplus (b\cdot x)=(a\oplus b)\cdot x$，因此第 $a$ 行异或后的方程是成立的。&lt;/p&gt;
&lt;p&gt;注意到异或方程组的增广矩阵是 $01$ 矩阵（矩阵中仅含有 $0$ 与 $1$），所以我们可以使用 C++ 中的 &lt;code&gt;std::bitset&lt;/code&gt; 进行优化，将时间复杂度降为 $O(\dfrac{n^2m}{\omega})$，其中 $n$ 为元的个数，$m$ 为方程条数，$\omega$ 一般为 $32$（与机器有关）。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bitset&amp;#x3C;MAXN&gt; A[MAXM];
int Gauss() {//A[][]是m行n列的矩阵
	for(int col = 1; col &amp;#x3C;= n; col++) {
		int x = col;
		while(x &amp;#x3C;= m &amp;#x26;&amp;#x26; !A[x][col]) x++;
		if(x == m + 1) return 0;
		ans = max(ans, x);
		if(col != x) {//swap(A[x], A[col]);
			bitset&amp;#x3C;MAXN&gt; tmp = A[x];
			A[x] = A[col]; A[col] = tmp;
		}
		for(int i = 1; i &amp;#x3C;= m; i++) {
			if(i != col &amp;#x26;&amp;#x26; A[i][col]) A[i] ^= A[col];
		}
	}
	return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;模意义下的高斯消元 - 模数为质数&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;对于模数为合数的情况，会在后面提到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;分析&lt;/h2&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//......
for(int i = 1; i &amp;#x3C;= n; i++) {
    if(i == row) continue;
    double t = A[i][col] / A[row][col];
    for(int j = 1; j &amp;#x3C;= m + 1; j++) {
        A[i][j] -= A[row][j] * t;
    }
}
//......
for(int i = 1; i &amp;#x3C;= n; i++) {
    ans[i] = A[i][n + 1] / A[i][i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模意义下的&lt;/strong&gt;高斯-约旦消元：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//......
for(int i = 1; i &amp;#x3C;= n; i++) {
	if(i == row) continue;
	int k = A[i][col] * inv(a[col][col]) % mod;
	for(int j = 1; j &amp;#x3C;= m + 1; j++) {
		A[i][j] = (A[i][j] + A[col][j] * (mod - k)) % mod;
	}
}
//......
for(int i = 1; i &amp;#x3C;= n; i++) {
    ans[i] = A[i][n + 1] * inv(A[i][i]) % mod;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;模意义下的高斯消元 - 模数为合数&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;模数为合数时，有些数没有逆元，需要另寻方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;对于模数为合数的情况，我们使用类似&lt;strong&gt;辗转相除&lt;/strong&gt;的方法。&lt;/p&gt;
&lt;p&gt;对于两行 $a, b$，现在要消去变元 $c$。&lt;/p&gt;
&lt;p&gt;在辗转相除法时，我们会用 $a\leftarrow a\bmod b$，但现在要同时操作一整行，没法直接取模，所以要模拟一下取模的过程。&lt;/p&gt;
&lt;p&gt;我们每次将 $a$ 减去 $b$ 的尽可能多倍（$k\in \Bbb{Z}^{+}$），使仍然保持 $a_c\geq 0$，之后交换 $a, b$，重复操作，直到 $a_c=0$.&lt;/p&gt;
&lt;p&gt;具体地，所减去的倍数 $k=\lfloor\dfrac{a_c}{b_c}\rfloor$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;辗转相除法就不用每次找最大的换上去了，因为不需要控制精度，都是整数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Gauss() {
	for(int col = 1; col &amp;#x3C;= n; col++) {
		for(int row = col + 1; row &amp;#x3C;= n; row++) {
			while(a[row][col]) {
				int t = a[col][col] / a[row][col];
				for(int i = 1; i &amp;#x3C;= n; i++) {
					a[col][i] = (a[col][i] + a[row][i] * (p - t)) % p;
				}
				for(int i = 1; i &amp;#x3C;= n; i++) swap(a[col][i], a[row][i]);
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;[JSOI2008] 球形空间产生器&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个 $n$ 维超球上的 $n+1$ 个点的坐标，求球心坐标。保证有唯一解。&lt;/p&gt;
&lt;p&gt;$n&amp;#x3C;=10$，坐标绝对值为 $20000$ 以内实数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;球心：到球面任意一点距离都相等的点。&lt;/p&gt;
&lt;p&gt;距离：若两点坐标为 $A(a_1, a_2\cdots, a_n)$，$B(b_1, b_2, \cdots, b_n)$，那么：
$$
\operatorname{dis(A, B)}=\sqrt{(a_1-b_1)^2+(a_2-b_2)^2+\cdots+(a_n-b_n)^2}
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;设圆心坐标为 $(x_1, x_2, \cdots, x_n)$，那么容易列出一个方程组：
$$
\left{
\begin{array}{}
\sum_{i=1}^n(a_{1, i}-x_i)^2=C^2 &amp;#x26;(1)\
\sum_{i=1}^n(a_{2, i}-x_i)^2=C^2 &amp;#x26;(2)\
\cdots&amp;#x26;\cdots\
\sum_{i=1}^n(a_{n, i}-x_i)^2=C^2&amp;#x26;(n)\
\sum_{i=1}^n(a_{n+1, i}-x_i)^2=C^2&amp;#x26;(n+1)\
\end{array}
\right.
$$
其中 $C$ 就代表 $n$ 维超球的半径。&lt;/p&gt;
&lt;p&gt;但这不是一个线性方程组，没法快速求解。&lt;/p&gt;
&lt;p&gt;又注意到，求出圆心坐标，只有 $n$ 个未知数，而现在能列出 $n+1$ 个方程，所以考虑进行&lt;strong&gt;差分&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;差分&lt;/h2&gt;
&lt;p&gt;对于其中的第 $p$ 个方程，我们对其进行整理，变为：
$$
\sum_{i=1}^{n}{{a_{p, i}}^2}+\sum_{i=1}^{n}{{x_i}^2}-\sum_{i=1}^{n}{2\cdot a_{p, i}\cdot x_i} \tag{p}
$$
同理有：
$$
\sum_{i=1}^{n}{{a_{p+1, i}}^2}+\sum_{i=1}^{n}{{x_i}^2}-\sum_{i=1}^{n}{2\cdot a_{p+1, i}\cdot x_i} \tag{p+1}
$$
我们用 $(p+1)-(p)$，得到：
$$
\sum_{i=1}^{n}{2(a_{p, i}-a_{p+1, i})x_i}=\sum_{i=1}^{n}{({a_{p, i}}^2-{a_{p+1, i}}^2)} \tag{&lt;em&gt;}
$$
以此类推，我们能得到 $n$ 个形如 $(&lt;/em&gt;)$ 的线性方程，于是问题可以用高斯消元求解。&lt;/p&gt;
&lt;p&gt;最终的系数矩阵为：
$$
\left[\begin{matrix}
2(a_{1, 1}-a_{2, 1}) &amp;#x26; 2(a_{1, 2}-a_{2, 2}) &amp;#x26; \cdots  &amp;#x26; 2(a_{1, n}-a_{2, n}) \
2(a_{2, 1}-a_{3, 1}) &amp;#x26; 2(a_{2, 2}-a_{3, 2}) &amp;#x26; \cdots  &amp;#x26; 2(a_{2, n}-a_{3, n}) \
\cdots               &amp;#x26; \cdots               &amp;#x26; \ddots  &amp;#x26; \cdots               \
2(a_{n, 1}-a_{n+1, 1}) &amp;#x26; 2(a_{n, 2}-a_{n+1, 2}) &amp;#x26; \cdots &amp;#x26; 2(a_{n, n}-a_{n+1, n})
\end{matrix} \middle|
\begin{matrix}
\sum_{j=1}^n({a_{1, j}}^2-{a_{2, j}}^2) \
\sum_{j=1}^n({a_{2, j}}^2-{a_{3, j}}^2) \
\vdots\
\sum_{j=1}^n({a_{n, j}}^2-{a_{n+1, j}}^2)
\end{matrix} \right]
$$&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://paste.ubuntu.com/p/DdF9c7gjcg/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;，应该不用看吧？&lt;/p&gt;
&lt;h1&gt;[SDOI2010] 外星千足虫&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定 $n$ 个点，按点权的奇偶性分为两类。&lt;/p&gt;
&lt;p&gt;给定 $m$ 次统计数据，每条数据包含：若干个&lt;strong&gt;被选中点&lt;/strong&gt;，以及&lt;strong&gt;被选中的点权和的奇偶性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;请判断每个点权的奇偶性。&lt;/p&gt;
&lt;p&gt;题目保证有解，若解不唯一，输出 $\rm{Cannot;Determine}$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;设 $a_{i, j}\in{0, 1}$ 表示第 $i$ 条数据中，第 $j$ 个点是否被选中， $b_i\in {0, 1}$ 表示第 $i$ 次统计结果的奇偶性。&lt;/p&gt;
&lt;p&gt;设 $x_i=w_i\bmod 2$ 表示第 $i$ 个点权的奇偶性。&lt;/p&gt;
&lt;p&gt;那么有同余方程组：
$$
\left{
\begin{array}{}
a_{1, 1}x_1+a_{1, 2}x_2+\cdots+a_{1, n}x_n=b_1\pmod{2} &amp;#x26;&amp;#x26;(1)\
a_{2, 1}x_1+a_{2, 2}x_2+\cdots+a_{2, n}x_n=b_2\pmod{2} &amp;#x26;&amp;#x26;(2)\
\cdots                                                 &amp;#x26;&amp;#x26;\cdots\
a_{m, 1}x_1+a_{m, 2}x_2+\cdots+a_{m, n}x_n=b_m\pmod{2}&amp;#x26;&amp;#x26;(m)\
\end{array}
\right.
$$
注意到，方程左右两侧都&lt;strong&gt;只需要考虑二进制下的第一位&lt;/strong&gt;，因此可以转化为一个异或方程组：
$$
\left{
\begin{array}{}
a_{1, 1}x_1\oplus a_{1, 2}x_2\oplus \cdots \oplus a_{1, n}x_n=b_1 &amp;#x26;&amp;#x26;(1)\
a_{2, 1}x_1\oplus a_{2, 2}x_2\oplus \cdots \oplus a_{2, n}x_n=b_2 &amp;#x26;&amp;#x26;(2)\
\cdots                                                            &amp;#x26;&amp;#x26;\cdots\
a_{m, 1}x_1\oplus a_{m, 2}x_2\oplus \cdots \oplus a_{m, n}x_n=b_m &amp;#x26;&amp;#x26;(m)\
\end{array}
\right.
$$
解异或方程组即可。&lt;/p&gt;
&lt;h2&gt;其他要求&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;题目要求：输出一个不超过 $M$ 的正整数 $K$，表明在第 $K$ 次统计结束后就可以确定唯一解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于这个要求，我们只需要在高斯消元时每次对行取 $\max$ 即可，详见下面代码的第 $7$ 行。&lt;/p&gt;
&lt;p&gt;在下面代码的消元过程中，对于第 $\rm{col}$ 列，我们会贪心地找到编号最小的 $x$ 满足 $A[x][col]=1$.&lt;/p&gt;
&lt;p&gt;所以，要想确定方程的解，至少要通过前 $x$ 条统计数据，否则第 $\rm{col}$ 个变量就无法求解。&lt;/p&gt;
&lt;h1&gt;Band-Matrix&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;这是个在 OI 中很经典的 trick。&lt;/p&gt;
&lt;p&gt;Band-Matrix，带状矩阵，对这种矩阵进行&lt;strong&gt;高斯消元&lt;/strong&gt;时，可以进行优化。&lt;/p&gt;
&lt;p&gt;顾名思义，带状矩阵是长条形的，差不多是这样：&lt;/p&gt;
&lt;p&gt;白色部分为 $0$，红色部分有数，其它部分先不管。&lt;/p&gt;
&lt;p&gt;这样中间形成了一个宽度为 $d$ 的带，这样消元的复杂度可以降到 $\mathcal{O}(nd^2)$.&lt;/p&gt;
&lt;p&gt;常见于一些期望DP的问题。&lt;/p&gt;
&lt;h2&gt;消元流程&lt;/h2&gt;
&lt;p&gt;在图中，绿色节点是我们要保留的主元。&lt;/p&gt;
&lt;p&gt;注意到在这种形式下，对于任意的 $A_{i, i}$，其向下、向右的&lt;strong&gt;有效数字个数均 $\leq d-1$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;所以假设现在要消第 $i $ 列，那么从第 $i$ 行开始往下枚举 $d-1$ 行，每行往右消 $d$ 个数字，最后能得到一个倒三角矩阵。&lt;/p&gt;
&lt;p&gt;之后回代。回代过程的复杂度最高是 $\mathcal{O}(n^2)$ 的，不是复杂度瓶颈，但也可以根据带状矩阵的形状优化。&lt;/p&gt;
&lt;h3&gt;判断无解/存在自由元&lt;/h3&gt;
&lt;p&gt;大部分题好像都有解，下面的参考资料中也有讲。&lt;/p&gt;
&lt;h3&gt;细节&lt;/h3&gt;
&lt;p&gt;如果 $n$ 太大，二维数组开不出来，可以只记录带状矩阵的部分，也可以用 &lt;code&gt;unordered_map&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/froggy/qian-tan-gao-si-xiao-yuan-ta-zhan-zhi-band-matrix&quot;&gt;浅谈高斯消元拓展之 band-matrix - Froggy 珂学家的博客&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;P5516 [MtOI2019]小铃的烦恼&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定长度为 $n$ 的序列，第 $i$ 个元素为 $a_i$.&lt;/p&gt;
&lt;p&gt;每次随机取出一个有序数对 $(x, y)(x\neq y)$，然后以 $s_{x, y}$ 的成功概率将 $a_x$ 变为 $a_y$（若不成功则不变）。&lt;/p&gt;
&lt;p&gt;要让全部元素都相同，求期望次数。&lt;/p&gt;
&lt;p&gt;$1\leq a_i \leq26$，$n\leq 2\times 10^3$.&lt;/p&gt;
&lt;p&gt;对于所有数据，满足 $(\sum\limits_{a=1}^{n}\sum\limits_{b=1}^n s_{a, b})=n^2$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;高斯消元？看不出来有方程。&lt;/p&gt;
&lt;p&gt;由于 $(\sum\limits_{a=1}^{n}\sum\limits_{b=1}^n s_{a, b})=n^2$ ，显然每个 $s_{i, j}$ 都恒为 $1$。这表明对于选出的任意两个数 $x, y$，&lt;strong&gt;每次操作都能成功&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然而这并没有使这题与概率无关，因为 $x, y$ 的选取仍然是随机的。&lt;/p&gt;
&lt;p&gt;考虑到最后全部的元素会相同，但不知道都变成什么元素，因此我们先枚举目标元素**（最后剩下的元素）。&lt;/p&gt;
&lt;p&gt;下面开始推式子。&lt;/p&gt;
&lt;h2&gt;概率&lt;/h2&gt;
&lt;p&gt;对序列的每次操作，可能会使目标元素的个数增加：$-1/0/1$ 个。&lt;/p&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;设 $p[i]$ 表示：&lt;strong&gt;此时有 $i$ 个目标元素，全部元素都变成当前选取的目标元素的概率。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么：
$$
p[i]=\frac{i(n-i)}{n(n-1)}p[i-1]+\frac{i(n-i)}{n(n-1)}p[i+1]+(1-2\frac{i(n-i)}{n(n-1)})p[i]
$$
方程代表：从有 $i$ 个目标元素的状态为基础，&lt;strong&gt;操作一次&lt;/strong&gt;，产生的三种可能情况。&lt;/p&gt;
&lt;p&gt;要让目标元素个数 $\pm 1$，就要从 $i$ 个数中选一个，再从其他的 $n-i$ 个数中选一个。&lt;/p&gt;
&lt;p&gt;从 $n$ 个数中随机选取两个的方案数是 $n(n-1)$，因此目标元素个数 $\pm 1$ 的概率均为 $\frac{i(n-i)}{n(n-1)}$.&lt;/p&gt;
&lt;p&gt;而目标元素个数不变的概率，就是 $1$ 减去前两种情况剩下的概率。&lt;/p&gt;
&lt;p&gt;对方程进行整理，设 $k=\frac{i(n-1)}{n(n-1)}$，那么有：
$$
\begin{aligned}
p[i] &amp;#x26;= k\cdot p[i-1]+k\cdot p[i+1]+(1-2k)p[i]\
2k\cdot p[i] &amp;#x26;= k\cdot p[i-1]+k\cdot p[i+1]\
\end{aligned}
$$
把 $k$ 约掉：
$$
2p[i]=p[i-1]+p[i+1]
$$
也就是：
$$
p[i+1]-p[i]=p[i]-p[i-1]
$$
这说明 $p$ 的相邻两项差值相同，&lt;strong&gt;$p$ 是等差数列&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;又因为显然， $p[0]=0$，$p[n]=1$，可以得到一个非常简单的形式：
$$
p[i]=\frac{i}{n}
$$
在此基础上，我们继续研究期望。&lt;/p&gt;
&lt;h2&gt;期望&lt;/h2&gt;
&lt;p&gt;期望的分析要简单一些，设 $f[i]$ 表示：&lt;strong&gt;当前有 $i$ 个目标元素，使全部元素都变成目标元素的期望步数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们只关心能使目标元素个数&lt;strong&gt;变化&lt;/strong&gt;的操作。可以发现，目标元素变化（$+1/-1$）的概率为 $2\frac{i(n-i)}{n(n-1)}$.&lt;/p&gt;
&lt;p&gt;因此，操作一次，期望变化 $\frac{2i(n-i)}{n(n-1)}$；变化为 $1$，期望次数就是 $\frac{n(n-1)}{2i(n-i)}$.&lt;/p&gt;
&lt;p&gt;所以得到和期望相关的方程：
$$
p[i]f[i]=p[i]\cdot\frac{n(n-1)}{2i(n - i)} + \frac{1}{2}p[i - 1]f[i - 1]+\frac{1}{2}p[i + 1]f[i + 1]
$$
方程整体的含义是：当前状态下有 $i$ 个目标元素，要让个数变化 $1$。第一项就是变化 $1$ 的&lt;strong&gt;期望操作次数&lt;/strong&gt;，后两项分别是个数 $+1/-1$ 后的情况。&lt;/p&gt;
&lt;p&gt;$f[i]$ 乘上了 $p[i]$，因为 $p[i]$ 的概率最终能移动到目标元素，$f[i]$ 的期望贡献只有 $p[i]\times f[i]$。&lt;/p&gt;
&lt;p&gt;由于 $+1/-1$ 的概率相同，所以两种情况的系数都是 $\frac{1}{2}$.&lt;/p&gt;
&lt;p&gt;同理，期望操作也要乘上 $p[i]$，因为只有 $p[i]$ 的概率，会向目标方向转移。（或者也可以把左边的 $p[i]$ 除过去，用另一种方法理解）&lt;/p&gt;
&lt;p&gt;把 $p[i]=\frac{i}{n}$ 代入，整理得到：
$$
\begin{aligned}
f[i] &amp;#x26;= \frac{n(n-1)}{2i(n-i)}+\frac{i-1}{2i}f[i-1]+\frac{i+1}{2i}f[i+1]\
f[i]-\frac{i-1}{2i}f[i-1]-\frac{i+1}{2i}f[i+1]&amp;#x26;= \frac{n(n-1)}{2i(n-i)}
\end{aligned}
$$
又因为 $f[n]=0$， $i\in[1, n-1]$.&lt;/p&gt;
&lt;p&gt;所以现在得到了 $n-1$ 个方程，$n-1$ 个未知数，可以高斯消元解方程组。&lt;/p&gt;
&lt;p&gt;然而本题数据范围 $n\leq 2\times 10^3$，$\mathcal{O}(n^3)$ 的高斯消元不可行，下面是一些消元技巧。&lt;/p&gt;
&lt;h2&gt;高斯消元&lt;/h2&gt;
&lt;p&gt;每个方程只有三项是有系数的，矩阵长这样：
$$
\begin{matrix}
&amp;#x26;&amp;#x26;\textcolor{gray}{0} &amp;#x26;\textcolor{gray}{1} &amp;#x26;\textcolor{gray}{2} &amp;#x26;\textcolor{gray}{3} &amp;#x26;\textcolor{gray}{4}\
\textcolor{white}{1}\
\textcolor{gray}{1} &amp;#x26;&amp;#x26;0 &amp;#x26;1 &amp;#x26;a &amp;#x26;0 &amp;#x26;0\
\textcolor{gray}{2} &amp;#x26;&amp;#x26;0 &amp;#x26;b &amp;#x26;1 &amp;#x26;c &amp;#x26;0\
\textcolor{gray}{3} &amp;#x26;&amp;#x26;0 &amp;#x26;0 &amp;#x26;d &amp;#x26;1 &amp;#x26;f\
\textcolor{gray}{4} &amp;#x26;&amp;#x26;0 &amp;#x26;0 &amp;#x26;0 &amp;#x26;g &amp;#x26;1
\end{matrix}
$$
可以发现，对第 $i$ 个元，只有$i-1$，$i$ 和 $i+1$ 行的系数非零。所以如果用高斯-约旦消元法，只需要消第 $i-1$ 和 $i+1$ 行的就行了。&lt;/p&gt;
&lt;p&gt;又因为每行只有三个变元的系数和一个等号右边的系数，所以这样的时间复杂度是 $\mathcal{O}(n)$.&lt;/p&gt;
&lt;p&gt;此外，空间复杂度也能比较轻易地优化到 $\mathcal{O}(n)$，可以参考其他题解。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://paste.ubuntu.com/p/w57MWzy4hV/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;概率都是 $1.00$，可以不用输入。注意数组要用 &lt;code&gt;double&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/a_forever_dream/article/details/105512284&quot;&gt;a_forever_dream的博客 - CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;P3232 - [HNOI2013]游走&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个 $n$ 个点 $m$ 条边的&lt;strong&gt;无向连通图&lt;/strong&gt;，顶点从 $1$ 编号到 $n$，边从 $1$ 编号到 $m$。&lt;/p&gt;
&lt;p&gt;小 Z 在该图上进行随机游走，初始时小 Z 在 $1$ 号顶点，每一步小 Z 以&lt;strong&gt;相等&lt;/strong&gt;的概率随机选择当前顶点的某条边，沿着这条边走到下一个顶点，获得等于这条&lt;strong&gt;边的编号&lt;/strong&gt;的分数。当小 Z 到达 $n$ 号顶点时游走结束，总分为所有获得的分数之和。&lt;/p&gt;
&lt;p&gt;现在，请你对这 $m$ 条边进行&lt;strong&gt;编号&lt;/strong&gt;，使得小 Z 获得的总分的期望值&lt;strong&gt;最小&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;求最小期望值。&lt;/p&gt;
&lt;p&gt;$2\leq n\leq 500$，$1\leq m\leq 125000$，无重边无自环。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;设边 $(i,j)$ 的期望经过次数为 $E_{(i, j)}$，答案即 $\sum E_{(i, j)}  v_{(i,  j)}$，因为每条边的边权 $v$ 可以自己定，容易想到贪心。&lt;/p&gt;
&lt;p&gt;先算出期望经过次数，然后&lt;strong&gt;让经过次数最大的边的编号最小&lt;/strong&gt;。就是一个期望DP。&lt;/p&gt;
&lt;h2&gt;期望DP&lt;/h2&gt;
&lt;p&gt;由于 $m$ 比较大，并且我们不会列出只和 $E$ 相关的方程式，所以先处理点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设 $f_i$ 表示从 $1$ 走到 $n$ 的过程中，经过点 $i$ 的期望次数。&lt;/strong&gt; 用其表示 $E$，
$$
E_{(i,j)}=[i\neq n]\frac{f_i}{\operatorname{deg_i}} + [j\neq n]\frac{f_j}{\operatorname{deg}_j}
$$
经过一条边有两种可能的方向，分别考虑从 $i\rightarrow j$ 和 $j\rightarrow i$。&lt;/p&gt;
&lt;p&gt;首先根据题意要求起点不能为 $n$，从 $i$ 出发，下一次恰好走到 $j$ 的概率即为 $\dfrac{1}{\operatorname{deg}_i}$。&lt;/p&gt;
&lt;p&gt;下面计算 $f_i$，和上面类似地，有：
$$
f_i=
\left{
\begin{array}{lr}
\sum_{(i, j)\in E,j\neq n}\frac{f_j}{\operatorname{deg}&lt;em&gt;j}+1, &amp;#x26;i = 1\
\sum&lt;/em&gt;{(i, j)\in E,j\neq n}\frac{f_j}{\operatorname{deg}_j},   &amp;#x26;i\neq1\
\end{array}
\right.
$$
相当于对于每个 $i$，枚举上一个经过的点 $j$，从 $j$ 走到 $i$ 的概率为 $\dfrac{1}{\operatorname{deg}_j}$.&lt;/p&gt;
&lt;p&gt;对于节点 $1$，初始时一定会经过一次，所以 $f_i$ 要多 $1$.&lt;/p&gt;
&lt;p&gt;对于节点 $n$，走到之后就不能出去了，所以枚举 $j$ 时 $j\neq n$.&lt;/p&gt;
&lt;p&gt;于是我们得到了 $n$ 个有关 $f$ 的方程，转移有后效性，用高斯消元求解即可。&lt;/p&gt;
&lt;p&gt;时间复杂度：$\mathcal{O}(n^3)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://paste.ubuntu.com/p/w57MWzy4hV/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;模型&lt;/h2&gt;
&lt;p&gt;这种模型在概率期望题中很常见，只不过表达形式有所不同。&lt;/p&gt;
&lt;p&gt;给定若干个点，点 $u$ 有 $P_{u, v}$ 的概率通向点 $v$。终点或某些其他点，进去之后就不能出来。&lt;/p&gt;
&lt;p&gt;求从点 $S$ 出发，走到点 $T$ 的期望步数。&lt;/p&gt;
&lt;p&gt;这个图如果不是 $\rm{DAG}$ 的话就不能用 DP 解决，只能列方程，用高斯消元。&lt;/p&gt;
&lt;p&gt;设 $E_i$ 表示从 $i$ 走到 $T$ 的期望步数，方程形如：
$$
E_i=
\left{
\begin{array}{lr}
\sum_{j=1}^n E_jP_{i, j} + 1, &amp;#x26;i\neq T\
0, &amp;#x26;i=T
\end{array}
\right.
$$
就是所有后继状态的期望步数乘上对应转移概率，再求和。&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/blog/ShadowassIIXVIIIIV/solution-p4457&quot;&gt;治疗之雨题解 - shadowice1984 的博客&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;CF24D - Broken robot&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;$n$ 行 $m$ 列的矩阵，$(1,1)$ 是左上角，$(n,m)$ 是右下角。&lt;/p&gt;
&lt;p&gt;初始状态在 $(x,y)$。每次等概率向&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;**等概率：**若其位于第一列，只能向右、下走或原地不动，则选择这三种的概率均为 $\frac{1}{3}$.&lt;/p&gt;
&lt;p&gt;求走到最后一行期望的步数，保留四位小数。&lt;/p&gt;
&lt;p&gt;$1\leq n, m\leq 10^3$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;令 $f_{i,j}$ 表示从位置 $(i,j)$ 走到最后一行，所需行动次数的期望值。目标是要求出 $f_{x, y}$.&lt;/p&gt;
&lt;p&gt;首先，初始状态为：$\forall j\in[1,m],f_{n,j}=0$。&lt;/p&gt;
&lt;p&gt;不在最后一行时：
$$
f_{i, j}=
\left{
\begin{array}{lc}
\frac{1}{3}(f_{i,j}+f_{i,j+1}+f_{i+1,j})+1, &amp;#x26;j=1\
\frac{1}{4}(f_{i,j}+f_{i,j-1}+f_{i,j+1}+f_{i+1,j})+1, &amp;#x26;1 &amp;#x3C; j &amp;#x3C; m\
\frac{1}{3}(f_{i,j}+f_{i,j-1}+f_{i+1,j})+1, &amp;#x26;j=m\
\end{array}
\right.
$$
在第一列不能再向左走，在第 $m$ 列不能再向右走。&lt;/p&gt;
&lt;p&gt;暴力高斯消元，时间复杂度 $\mathcal{O}(n^6)$，不能接受。空间复杂度 $\mathcal{O}(n^4)$，更不能接受。&lt;/p&gt;
&lt;h2&gt;消元&lt;/h2&gt;
&lt;p&gt;显然第 $i$ 行的状态只与 $i$ 和 $i+1$ 行有关，在行维度上，其满足无后效性。我们还知道 $f_n=0$，所以&lt;strong&gt;从下往上逐行计算&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是方程中 $f_{i+1,j}$ 是已知的常数，直接移到等式右侧，得到方程组：
$$
\left{\begin{array}{cc}
-\frac{2}{3}f_{i,j} + \frac{1}{3}f_{i,j+1} = -\frac{1}{3}f_{i+1,j}-1, &amp;#x26;j=1\
-\frac{3}{4}f_{i,j} + \frac{1}{4}f_{i,j-1} + \frac{1}{4}f_{i,j+1} = -\frac{1}{4}f_{i+1,j} - 1, &amp;#x26;1&amp;#x3C;j&amp;#x3C;m\
-\frac{2}{3}f_{i,j} + \frac{1}{3}f_{i,j-1} = -\frac{1}{3}f_{i+1,j}-1, &amp;#x26;j=m\
\end{array}\right.
$$
这样的复杂度变为 $\mathcal{O}(n^4)$。&lt;/p&gt;
&lt;p&gt;再观察，其系数矩阵只有主对角线附近有数，其余部分均为 $0$，比如 $m=5$ 时：
$$
\begin{bmatrix}
-\frac{2}{3} &amp;#x26; \frac{1}{3} &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; 0\
\frac{1}{4} &amp;#x26; -\frac{3}{4} &amp;#x26; \frac{1}{4} &amp;#x26; 0 &amp;#x26; 0\
0 &amp;#x26; \frac{1}{4} &amp;#x26; -\frac{3}{4} &amp;#x26; \frac{1}{4} &amp;#x26; 0\
0 &amp;#x26; 0 &amp;#x26; \frac{1}{4} &amp;#x26; -\frac{3}{4} &amp;#x26; \frac{1}{4}\
0 &amp;#x26; 0 &amp;#x26; 0 &amp;#x26; \frac{1}{3} &amp;#x26; -\frac{2}{3}\
\end{bmatrix}=\begin{bmatrix}
-\frac{1}{3}f_{i+1,j}-1\
-\frac{1}{4}f_{i+1,j}-1\
-\frac{1}{4}f_{i+1,j}-1\
-\frac{1}{4}f_{i+1,j}-1\
-\frac{1}{3}f_{i+1,j}-1\
\end{bmatrix}
$$
可以用 $\rm{Band-Matrix}$ 继续优化。&lt;/p&gt;
&lt;p&gt;这道题中，带的宽度可以看成常数。时间复杂度为 $\mathcal{O}(nm)$，空间复杂度为 $\mathcal{O}(m^2)$ 或 $\mathcal{O}(m)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;我的代码只保留了每行有用的三列：&lt;a href=&quot;https://paste.ubuntu.com/p/Gr6z68DQFs/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;，下面参考资料中的代码保留了整个矩阵。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/maoyiting/CF24D&quot;&gt;CF24D Broken robot - maoyiting&apos;s blog - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;CF963E - Circles of Waiting&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;在平面直角坐标系上，有一个神奇的点，一开始在 $(0, 0)$ 。&lt;/p&gt;
&lt;p&gt;每秒钟这个点都会随机移动，如果它目前在 $(x, y)$，那么下一秒它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 $(x - 1, y)$ 的概率是 $p_1$，&lt;/li&gt;
&lt;li&gt;在 $(x, y - 1)$ 的概率是 $p_2$，&lt;/li&gt;
&lt;li&gt;在 $(x + 1, y)$ 的概率是 $p_3$，&lt;/li&gt;
&lt;li&gt;在 $(x, y + 1)$ 的概率是 $p_4$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保证 $p_1 + p_2 + p_3 + p_4 = 1$，各次移动互不关联。&lt;/p&gt;
&lt;p&gt;求出这个点移动到&lt;strong&gt;距离原点距离大于 $R$ 的点&lt;/strong&gt;的期望步数，对 $10^9+7$ 取模。&lt;/p&gt;
&lt;p&gt;欧几里得距离，$0\leq R \leq 50$。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;先理解一下题意，下面是 &lt;a href=&quot;https://codeforces.com/blog/entry/58991&quot;&gt;CF官方题解&lt;/a&gt; 里的图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2022/03/05/dbe9738850193.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;只能在整点上移动，移出半径为 $R$ 的圆为止。&lt;/p&gt;
&lt;p&gt;期望DP，设 $f_{x, y}$ 为：从 $(x,y)$ 走出圆所需要的期望步数，方程显然：
$$
f_{x, y}=
\left{
\begin{array}{lc}
f_{x-1, y}p_1+f_{x, y-1}p_2+f_{x+1, y}p_3+f_{x, y+1}p_4+1, &amp;#x26;\sqrt{x^2+y^2}\leq R\
0, &amp;#x26;\sqrt{x^2+y^2}&gt; R
\end{array}
\right.
$$
高斯消元直接解的话，点数是略小于 $4R^2$，时间复杂度是 $\mathcal{O}(R^6)$.&lt;/p&gt;
&lt;p&gt;考虑利用 $\rm{Band-Matrix}$ 优化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/10/15/7dea4ab09cdd0.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果我们对圆内的点，从左到右，从上到下进行编号。若 $\operatorname{id}(x, y)=x$，则其四周点的 $\operatorname{id}\in[x-2R, x+2R]$.&lt;/p&gt;
&lt;p&gt;因此，第 $x$ 列，非零的范围是 $[x-2R, x+2R]$，矩阵是宽度为 $4R$ 的带状矩阵，&lt;/p&gt;
&lt;p&gt;消元时只需要考虑向下的 $2R$ 列和向右的 $2R$ 列，消成倒三角矩阵，再回代即可。&lt;/p&gt;
&lt;p&gt;这样的时间复杂度是 $\mathcal{O}(R^4)$ 的。未知数个数是 $R^2$ 级别的，消元是 $R\times R$ 级别的。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;代码实现上，注意坐标可能为负，细节比较多。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://paste.ubuntu.com/p/KXxbpv5Ndr/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;这篇比洛谷题解详细：&lt;a href=&quot;https://blog.csdn.net/lycheng1215/article/details/80180178&quot;&gt;UnnamedOrange - CSDN博客&lt;/a&gt;&lt;/p&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.bilibili.com/video/BV1ys411472E?p=4&quot;&gt;线性代数的本质 - 03 - 矩阵与线性变换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1ys411472E?p=4&quot;&gt;线性代数的本质 - 04 - 矩阵乘法与线性变换复合&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;矩阵求逆&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;求一个 $N\times N$ 的矩阵的逆矩阵。答案对 ${10}^9+7$ 取模。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;算法思想&lt;/h2&gt;
&lt;p&gt;从线性变换的角度理解矩阵和逆矩阵。&lt;/p&gt;
&lt;p&gt;逆矩阵表示：把原矩阵变到单位矩阵这一线性变换过程。因此，我们通过高斯消元把原矩阵变换为单位矩阵，同时新建一个矩阵记录变换过程。&lt;/p&gt;
&lt;h2&gt;算法流程&lt;/h2&gt;
&lt;p&gt;设 $A$ 是原矩阵，$A^{-1}$ 为 $A$ 的逆矩阵，则 $A\cdot A^{-1}=E_n$，$E_n$ 代表 $n$ 阶单位矩阵。
$$
E=\begin{bmatrix}
1      &amp;#x26;0      &amp;#x26;\cdots &amp;#x26;0      &amp;#x26;0      \
0      &amp;#x26;1      &amp;#x26;\cdots &amp;#x26;0      &amp;#x26;0      \
\cdots &amp;#x26;\cdots &amp;#x26;1      &amp;#x26;\cdots &amp;#x26;\cdots \
0      &amp;#x26;0      &amp;#x26;\cdots &amp;#x26;1      &amp;#x26;0      \
0      &amp;#x26;0      &amp;#x26;\cdots &amp;#x26;0      &amp;#x26;1
\end{bmatrix}
$$
若 我们&lt;strong&gt;把一个单位矩阵 $E_n$ 并在 $A$ 的右边&lt;/strong&gt;，之后通过高斯-约旦消元法，把 $A$ 消元成 $E_n$ 的形式，此时 $B$ 就是矩阵 $A$ 的逆。&lt;/p&gt;
&lt;p&gt;**具体地，**以 $n=5$ 为例，把两个矩阵合到一起：
$$
\begin{bmatrix}
A &amp;#x26; E_n
\end{bmatrix}=
\left[
\begin{matrix}
a_{1, 1} &amp;#x26;a_{1, 2} &amp;#x26;a_{1, 3} &amp;#x26;a_{1, 4} &amp;#x26;a_{1, 5}    \
a_{2, 1} &amp;#x26;a_{2, 2} &amp;#x26;a_{2, 3} &amp;#x26;a_{2, 4} &amp;#x26;a_{2, 5}    \
a_{3, 1} &amp;#x26;a_{3, 2} &amp;#x26;a_{3, 3} &amp;#x26;a_{3, 4} &amp;#x26;a_{3, 5}    \
a_{4, 1} &amp;#x26;a_{4, 2} &amp;#x26;a_{4, 3} &amp;#x26;a_{4, 4} &amp;#x26;a_{4, 5}    \
a_{5, 1} &amp;#x26;a_{5, 2} &amp;#x26;a_{5, 3} &amp;#x26;a_{5, 4} &amp;#x26;a_{5, 5}
\end{matrix}
\middle|
\begin{matrix}
1 &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;    \
&amp;#x26;1 &amp;#x26;  &amp;#x26;  &amp;#x26;    \
&amp;#x26;  &amp;#x26;1 &amp;#x26;  &amp;#x26;    \
&amp;#x26;  &amp;#x26;  &amp;#x26;1 &amp;#x26;    \
&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;1
\end{matrix}
\right]
$$
通过高斯消元，把左半部分矩阵变成单位矩阵：
$$
\begin{bmatrix}
E_n &amp;#x26; A^{-1}
\end{bmatrix}=
\left[
\begin{matrix}
1 &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;    \
&amp;#x26;1 &amp;#x26;  &amp;#x26;  &amp;#x26;    \
&amp;#x26;  &amp;#x26;1 &amp;#x26;  &amp;#x26;    \
&amp;#x26;  &amp;#x26;  &amp;#x26;1 &amp;#x26;    \
&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;1
\end{matrix}
\middle|
\begin{matrix}
b_{1, 1} &amp;#x26;b_{1, 2} &amp;#x26;b_{1, 3} &amp;#x26;b_{1, 4} &amp;#x26;b_{1, 5}    \
b_{2, 1} &amp;#x26;b_{2, 2} &amp;#x26;b_{2, 3} &amp;#x26;b_{2, 4} &amp;#x26;b_{2, 5}    \
b_{3, 1} &amp;#x26;b_{3, 2} &amp;#x26;b_{3, 3} &amp;#x26;b_{3, 4} &amp;#x26;b_{3, 5}    \
b_{4, 1} &amp;#x26;b_{4, 2} &amp;#x26;b_{4, 3} &amp;#x26;b_{4, 4} &amp;#x26;b_{4, 5}    \
b_{5, 1} &amp;#x26;b_{5, 2} &amp;#x26;b_{5, 3} &amp;#x26;b_{5, 4} &amp;#x26;b_{5, 5}
\end{matrix}
\right]
$$
此时矩阵 $B=A^{-1}$.&lt;/p&gt;
&lt;h1&gt;行列式&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个 $n$ 阶行列式 $A$，求 $|A|$。结果对 $p$ 取模。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;行列式&lt;/h2&gt;
&lt;p&gt;对于一个 $n$ 阶矩阵，有：&lt;/p&gt;
&lt;p&gt;$\operatorname{det}A=\sum\limits_{p}(-1)^{\tau(p)}\prod\limits_{i=1}^na_{i,p_i} $，其中 $p$ 是任意一个排列，$\tau(p)$ 指的是 $p$ 中的逆序对数。但这个定义暂时没啥用。&lt;/p&gt;
&lt;p&gt;矩阵 $A$ 的行列式可以表达为 $|A|$.&lt;/p&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;p&gt;建议先看 3b1b 的视频：&lt;a href=&quot;https://www.bilibili.com/video/BV1ys411472E?p=7&quot;&gt;线性代数的本质 - 05 - 行列式&lt;/a&gt;，再看下面的性质。&lt;/p&gt;
&lt;p&gt;以下内容参考 &lt;a href=&quot;https://cdn.tonyyin.top/2021/10/16/6ccbfd6c0b74e.pdf&quot;&gt;MIT公开课 - 行列式笔记&lt;/a&gt; 和 &lt;a href=&quot;https://www.bilibili.com/video/BV1zx411g7gq&quot;&gt;线性代数公开课 - MIT 18.06 Linear Algebra - 系列合集 - 哔哩哔哩&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;性质 $1$&lt;/h4&gt;
&lt;p&gt;$|E|=1$，$E$ 为单位矩阵。直接用代数定义就可以证明。&lt;/p&gt;
&lt;h4&gt;性质 $2$&lt;/h4&gt;
&lt;p&gt;**交换矩阵的两行，行列式的值变为相反数。**也直接用代数方法证明。&lt;/p&gt;
&lt;h4&gt;性质 $3.1$&lt;/h4&gt;
&lt;h1&gt;矩阵中的一行乘系数 $t$，等价于整个行列式乘系数 $t$，即：
$$
t\cdot
\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 &amp;#x26; c_2 &amp;#x26; c_3\
\end{array}\right|&lt;/h1&gt;
&lt;p&gt;\left|\begin{array}{ccc}
t\cdot a_1 &amp;#x26; t\cdot a_2 &amp;#x26; t\cdot a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 &amp;#x26; c_2 &amp;#x26; c_3\
\end{array}\right|
$$&lt;/p&gt;
&lt;h4&gt;性质 $3.2$&lt;/h4&gt;
&lt;h1&gt;对于每一行而言，行列式是线性的，即：
$$
\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 &amp;#x26; c_2 &amp;#x26; c_3\
\end{array}\right|
+
\left|\begin{array}{ccc}
d_1 &amp;#x26; d_2 &amp;#x26; d_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 &amp;#x26; c_2 &amp;#x26; c_3\
\end{array}\right|&lt;/h1&gt;
&lt;p&gt;\left|\begin{array}{ccc}
a_1+d_1 &amp;#x26; a_2+d_2 &amp;#x26; a_3+d_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 &amp;#x26; c_2 &amp;#x26; c_3\
\end{array}\right|
$$
注意 $|A+B|\neq|A|+|B|$.&lt;/p&gt;
&lt;h4&gt;性质 $4$&lt;/h4&gt;
&lt;p&gt;若存在两行相等，则行列式为 $0$；&lt;strong&gt;若存在两行成倍数关系，则行列式为 $0$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;证明：交换这两行，行列式值变号，又因为行列式值不变，所以 $\operatorname{det}=0$.&lt;/p&gt;
&lt;h4&gt;性质 $5$&lt;/h4&gt;
&lt;p&gt;**消元不影响行列式的值。**消元：第 $i$ 行减去第 $j$ 行的若干倍。
$$
\begin{array}{cl}
&amp;#x26;\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1 - k\cdot a_1&amp;#x26; c_2 - k\cdot a_2&amp;#x26; c_3-k\cdot a_3\
\end{array}\right|\
\underset{\text{Theory 3.2}}{\longrightarrow}&amp;#x26;
\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1\cdot a_1&amp;#x26; c_2&amp;#x26; c_3\
\end{array}\right|
+
\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
-k\cdot a_1&amp;#x26; - k\cdot a_2 &amp;#x26;-k\cdot a_3\
\end{array}\right|\
\underset{\text{Theory 4}}{\longrightarrow}&amp;#x26;
\left|\begin{array}{ccc}
a_1 &amp;#x26; a_2 &amp;#x26; a_3\
b_1 &amp;#x26; b_2 &amp;#x26; b_3\
c_1\cdot a_1&amp;#x26; c_2&amp;#x26; c_3\
\end{array}\right|
+
0
\end{array}
$$&lt;/p&gt;
&lt;h4&gt;性质 $6$&lt;/h4&gt;
&lt;p&gt;若行列式有一整行为 $0$，则行列式为 $0$。&lt;/p&gt;
&lt;p&gt;是性质 $3$ 中的 $t=0$ 的特殊情况。&lt;/p&gt;
&lt;h4&gt;性质 $7$&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;倒三角矩阵行列式的值，等于对角线元素的乘积，所以只与对角线上的数有关。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\left|\begin{array}{ccc}
x &amp;#x26; x &amp;#x26; x\
&amp;#x26; x &amp;#x26; x\
&amp;#x26;   &amp;#x26; x\
\end{array}\right|
\underset{\text{Eliminate}}{\longrightarrow}
\left|\begin{array}{ccc}
x &amp;#x26;   &amp;#x26; \
&amp;#x26; x &amp;#x26; \
&amp;#x26;   &amp;#x26; x\
\end{array}\right|
$$&lt;/p&gt;
&lt;p&gt;形如上面的矩阵，通过消元转化为更简形式即可。&lt;/p&gt;
&lt;h2&gt;求解&lt;/h2&gt;
&lt;p&gt;根据性质 $5$ 和性质 $7$，直接高斯消元即可。注意每次交换行时，需要$\operatorname{Ans}\leftarrow -\operatorname{Ans}$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int Gauss() {
	int det = 1;
	for(int col = 1; col &amp;#x3C;= n; col++) {
		for(int row = col + 1; row &amp;#x3C;= n; row++) {
			while(a[row][col]) {
				int t = a[col][col] / a[row][col];
				for(int i = 1; i &amp;#x3C;= n; i++) {
					a[col][i] = (a[col][i] + a[row][i] * (p - t)) % p;
                //  a[col][i] = (a[col][i] - a[row][i] * t      ) % p;
				}
				for(int i = 1; i &amp;#x3C;= n; i++) swap(a[col][i], a[row][i]);
				det *= -1;
			}
		}
	}
	for(int i = 1; i &amp;#x3C;= n; i++) det = det * a[i][i] % p;
    return (det + p) % p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;LGV引理&lt;/h1&gt;
&lt;h2&gt;LGV引理&lt;/h2&gt;
&lt;p&gt;参考 &lt;a href=&quot;http://oi-wiki.com/graph/lgv/&quot;&gt;OI Wiki&lt;/a&gt;，OIer 不需要证明，能完全理解定理内容就不错了。&lt;/p&gt;
&lt;h3&gt;定义如下&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$G$，一张带有边权的&lt;strong&gt;有向无环图&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$v_e$， 边 $e$ 的&lt;strong&gt;边权&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$\omega(P)$，对于一个依次经过 $u_1,u_2\cdots, u_k$ 的有向路径 $P(u_1\rightsquigarrow u_k)$，$\omega(P)$ 表示其所有&lt;strong&gt;边权之积&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;$e(u, v)=\sum\limits_{P: u\rightsquigarrow v}\omega(P)$，表示 $u$ 到 $v$ 的&lt;strong&gt;每一条&lt;/strong&gt;路径 $P$ 的 $\omega(P)$ &lt;strong&gt;之和&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$A={a_1, a_2,\cdots a_n}$，&lt;strong&gt;起点集合&lt;/strong&gt;；$B={b_1, b_2,\cdots b_n}$，终点集合。&lt;/li&gt;
&lt;li&gt;$S={S_1,S_2,\cdots,S_n}$，是路径的集合，代表 $n$ 条 $A\rightarrow B$ 的&lt;strong&gt;不相交路径&lt;/strong&gt;。形式化地，可以用一个排列 $\sigma(S)$ 来表示，使得 $S_i$ 代表路径 $A_i\rightsquigarrow B_{\sigma(S)_i}$，这组路径满足：$\forall i\neq j$，$S_i$ 和 $S_j$ 无公共点。&lt;/li&gt;
&lt;li&gt;$N(\sigma(S))$，表示排列 $\sigma(S)$ 的逆序对个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;引理&lt;/h3&gt;
&lt;p&gt;对于矩阵：
$$
M =
\begin{bmatrix}e(A_1,B_1)&amp;#x26;e(A_1,B_2)&amp;#x26;\cdots&amp;#x26;e(A_1,B_n)\ e(A_2,B_1)&amp;#x26;e(A_2,B_2)&amp;#x26;\cdots&amp;#x26;e(A_2,B_n)\
\vdots&amp;#x26;\vdots&amp;#x26;\ddots&amp;#x26;\vdots\
e(A_n,B_1)&amp;#x26;e(A_n,B_2)&amp;#x26;\cdots&amp;#x26;e(A_n,B_n)
\end{bmatrix}
$$
其行列式满足：
$$
\det(M)=\sum\limits_{S:A\rightarrow B}(-1)^{N(\sigma(S))}\prod\limits_{i=1}^n \omega(S_i)
$$
其中 $S:A\rightarrow B$ 代表：枚举每一组路径（满足上文要求的）。&lt;/p&gt;
&lt;p&gt;证明略，下面两道题会说到 LGV 引理的应用方法，&lt;/p&gt;
&lt;h2&gt;模板题&lt;/h2&gt;
&lt;h3&gt;题意&lt;/h3&gt;
&lt;p&gt;有一个 $n\times n$ 的棋盘，左下角为 $(1,1)$，右上角为 $(n,n)$，若一个棋子在点 $(x,y)$，那么走一步只能走到 $(x+1,y)$ 或 $(x,y+1)$。&lt;/p&gt;
&lt;p&gt;现在有 $m$ 个棋子，第 $i$ 个棋子一开始放在 $(a_i,1)$，最终要走到 $(b_i,n)$。问有多少种方案，使得每个棋子都能从起点走到终点，且对于所有棋子，走过路径上的点互不相交。输出方案数 $\bmod\ 998244353$ 的值。&lt;/p&gt;
&lt;p&gt;两种方案不同当且仅当存在至少一个棋子所经过的点不同。&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;方格图，每次只能向下或向右，不妨设 $a_i$ 和 $b_i$ 均为单调上升的。&lt;/p&gt;
&lt;p&gt;因为引理要求&lt;strong&gt;路径不相交&lt;/strong&gt;，所以&lt;strong&gt;唯一&lt;/strong&gt;的 $\sigma(S)=(1, 2,\cdots, n)$，$N(\sigma(S))=0$，也就是 $a_i$ 和 $b_i$ 配对。&lt;/p&gt;
&lt;p&gt;设每条边的边权均为 $1$，则 $\forall \omega(e)=1$，所以：
$$
\det(M)=\sum\limits_{S:A\rightarrow B}(-1)^{N(\sigma(S))}\prod\limits_{i=1}^n \omega(S_i)=\sum\limits_{S:A\rightarrow B}(-1)^0=\sum\limits_{S:A\rightarrow B}1
$$
&lt;strong&gt;右式即为题目所求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构造矩阵，$e(a_i,b_j)=\binom{b_j-a_i+n-1}{n-1}$，高斯消元求其行列式即可。&lt;/p&gt;
&lt;h2&gt;[NOI2021] 路径交点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P7736&quot;&gt;题面&lt;/a&gt; 太长就不放了。&lt;/p&gt;
&lt;p&gt;同样地，设每条边的边权均为 $1$，则 $\forall \omega(e)=1$，所以：
$$
\det(M)=\sum\limits_{S:A\rightarrow B}(-1)^{N(\sigma(S))}\prod\limits_{i=1}^n \omega(S_i)=\sum\limits_{S:A\rightarrow B}(-1)^{N(\sigma(S))}
$$
引理：&lt;strong&gt;两条路径交点个数的奇偶性，与将其起点和终点直接相连的交点个数的奇偶性相同。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，若我们用 LGV 引理的 $\sigma(S)$ 表示一组路径，若 $N(\sigma(S))\bmod 2=0$，则有路径偶数个交点，反之亦然。&lt;/p&gt;
&lt;p&gt;本题求：偶数个交点的方案数量 $-$ 奇数个交点的方案数量，这恰好对应式子中的 $(-1)^{N(\sigma(S))}$.&lt;/p&gt;
&lt;p&gt;因此，直接构造矩阵 $M$ 并求行列式，$e(i, j)$ 表示从第一层 $i$ 点到最后一层 $j$ 点的路径数量。&lt;/p&gt;
&lt;p&gt;求路径数量可以DP，或者记忆化搜索，不是这道题的难点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考资料：&lt;/strong&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/ix-35/solution-p7736&quot;&gt;&lt;strong&gt;NOI 2021 D1T2 题解 - ix35_ 的博客 - 洛谷博客&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Matrix-Tree 定理&lt;/h1&gt;
&lt;h2&gt;基尔霍夫矩阵&lt;/h2&gt;
&lt;p&gt;基尔霍夫矩阵，又称拉普拉斯矩阵，用于描述一张图的某些属性。&lt;/p&gt;
&lt;h3&gt;无向图&lt;/h3&gt;
&lt;p&gt;给定一张简单无向图 $G$，其包含 $n$ 个顶点 $v_1, v_2\cdots, v_n$，其拉普拉斯矩阵 $L$ 为：
$$
L_{i,j}=
{\begin{cases}\deg(v_{i})&amp;#x26;{\mbox{if}}\ i=j\-1&amp;#x26;{\mbox{if}}\ i\neq j\ {\mbox{and}}\ v_{i}{\mbox{ is adjacent to }}v_{j}\0&amp;#x26;{\mbox{otherwise}}\end{cases}}
$$
也就是说，若设 $D$ 为图的度数矩阵，$A$ 为图的邻接矩阵，则等价地有：
$$
L=D-A
$$&lt;/p&gt;
&lt;p&gt;下面是一个对于无向图的例子：&lt;/p&gt;
&lt;p&gt;|                           度数矩阵                           |                           邻接矩阵                           |                         基尔霍夫矩阵                         |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| ${\textstyle \left({\begin{array}{rrrrrr}2&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;0\0&amp;#x26;3&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;2&amp;#x26;0&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;0&amp;#x26;3&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;3&amp;#x26;0\0&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;0&amp;#x26;1\\end{array}}\right)}$ | ${\textstyle \left({\begin{array}{rrrrrr}0&amp;#x26;1&amp;#x26;0&amp;#x26;0&amp;#x26;1&amp;#x26;0\1&amp;#x26;0&amp;#x26;1&amp;#x26;0&amp;#x26;1&amp;#x26;0\0&amp;#x26;1&amp;#x26;0&amp;#x26;1&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;1&amp;#x26;0&amp;#x26;1&amp;#x26;1\1&amp;#x26;1&amp;#x26;0&amp;#x26;1&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;0&amp;#x26;1&amp;#x26;0&amp;#x26;0\\end{array}}\right)}$ | ${\textstyle \left({\begin{array}{rrrrrr}2&amp;#x26;-1&amp;#x26;0&amp;#x26;0&amp;#x26;-1&amp;#x26;0\-1&amp;#x26;3&amp;#x26;-1&amp;#x26;0&amp;#x26;-1&amp;#x26;0\0&amp;#x26;-1&amp;#x26;2&amp;#x26;-1&amp;#x26;0&amp;#x26;0\0&amp;#x26;0&amp;#x26;-1&amp;#x26;3&amp;#x26;-1&amp;#x26;-1\-1&amp;#x26;-1&amp;#x26;0&amp;#x26;-1&amp;#x26;3&amp;#x26;0\0&amp;#x26;0&amp;#x26;0&amp;#x26;-1&amp;#x26;0&amp;#x26;1\\end{array}}\right)}$ |&lt;/p&gt;
&lt;h3&gt;有向图&lt;/h3&gt;
&lt;p&gt;有向图存在两个基尔霍夫矩阵，分别为&lt;strong&gt;出度基尔霍夫矩阵&lt;/strong&gt; $L^\text{out}$ 和&lt;strong&gt;入度基尔霍夫矩阵&lt;/strong&gt; $L^\text{in}$.&lt;/p&gt;
&lt;p&gt;其含义与无向图类似，设图 $G$ 的出度矩阵为 $D^\text{out}$，入度矩阵为 $D^\text{in}$，则有：
$$
\begin{aligned}
L^\text{out}&amp;#x26;=D^\text{out}-A\
L^\text{in}&amp;#x26;=D^\text{in}-A
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;定理内容&lt;/h2&gt;
&lt;p&gt;规定：两个生成树不同，当且仅当存在一条边的被包含情况不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定理仅适用于无重边、无自环的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;无向图&lt;/h3&gt;
&lt;p&gt;设 $t(G)$ 为无向连通图 $G$ 的生成树个数。&lt;/p&gt;
&lt;p&gt;设 $L$ 为无向连通图 $G$ 的基尔霍夫矩阵，&lt;strong&gt;任选 $i$，从 $L$ 中删去第 $i$ 行和第 $i$ 列，构造出矩阵 $L^{\prime}$&lt;/strong&gt;，则：
$$
t(G)=\det L^{\prime}
$$
如图 $G$ 的基尔霍夫矩阵为 $L=\left[{\begin{array}{rrrr}2&amp;#x26;-1&amp;#x26;-1&amp;#x26;0\-1&amp;#x26;3&amp;#x26;-1&amp;#x26;-1\-1&amp;#x26;-1&amp;#x26;3&amp;#x26;-1\0&amp;#x26;-1&amp;#x26;-1&amp;#x26;2\end{array}}\right]$，则 $L^{\prime}$ 可以为 $L^{\prime}=\left[{\begin{array}{rrrr}2&amp;#x26;-1&amp;#x26;-1&amp;#x26;0\-1&amp;#x26;3&amp;#x26;-1&amp;#x26;-1\-1&amp;#x26;-1&amp;#x26;3&amp;#x26;-1\0&amp;#x26;-1&amp;#x26;-1&amp;#x26;2\end{array}}\right]$.&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;设 $t^\text{root}(G,s)$ 为以 $s$ 为根节点的根向生成树个数， $t^\text{leaf}(G,s)$ 为以 $s$ 为根节点的叶向生成树个数。&lt;/p&gt;
&lt;p&gt;设 $L^\text{out}$ 和 $L^\text{in}$ 为有向连通图 $G$ 的基尔霍夫矩阵，&lt;strong&gt;从中删去第 $k$ 行第 $k$ 列后的矩阵为 ${L^\text{out}}^{\prime}(k)$ 和 ${L^\text{in}}^{\prime}(k)$&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;钦定 $k$ 为所统计生成树的根节点，则有：
$$t^\text{root}(G,s)=\det{L^\text{out}}^{\prime}(s)$$&lt;/p&gt;
&lt;p&gt;$$t^\text{leaf}(G,s)=\det{L^\text{in}}^{\prime}(s)$$&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;有向图的根向树形图个数为 $\sum_{u} t^\text{root}(G,u)$，叶向同理&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;简要证明&lt;/h2&gt;
&lt;p&gt;参见 &lt;a href=&quot;https://en.wikipedia.org/wiki/Kirchhoff%27s_theorem#Proof_outline&quot;&gt;Kirchhoff&apos;s theorem - Proof outline - Wikipedia&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Laplacian_matrix&quot;&gt;Laplacian matrix - Wikipedia&lt;/a&gt; 和 &lt;a href=&quot;https://en.wikipedia.org/wiki/Kirchhoff%27s_theorem&quot;&gt;Kirchhoff&apos;s theorem - Wikipedia&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;还有例题：&lt;a href=&quot;https://www.luogu.com.cn/problem/P4111&quot;&gt;[HEOI2015] 小Z的房间&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;一些资料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;题单链接：&lt;/strong&gt;&lt;a href=&quot;https://www.luogu.com.cn/training/114026&quot;&gt;【2021赛季专题分享】高斯消元 - TonyYin - 洛谷 - 计算机科学教育新生态&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPT课件：&lt;/strong&gt;&lt;a href=&quot;https://cdn.tonyyin.top/uploads/Gauss.pdf&quot;&gt;课件 - PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参考资料：&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://oi-wiki.com/math/gauss/&quot;&gt;高斯消元 - OI Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;李煜东. 算法竞赛进阶指南. 中原出版传媒集团·河南电子音像出版社, 2018.&lt;/li&gt;
&lt;li&gt;页面注释部分提到的参考资料。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;课程资源：&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;3blue1brown：&lt;a href=&quot;https://www.bilibili.com/video/BV1ys411472E&quot;&gt;【官方双语_合集】线性代数的本质 - 系列合集_哔哩哔哩_bilibili&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gilbert Strang：&lt;a href=&quot;https://www.bilibili.com/video/BV1zx411g7gq&quot;&gt;线性代数公开课 - MIT 18.06 Linear Algebra - 系列合集 - 哔哩哔哩&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MIT课程提供的英文讲义：&lt;a href=&quot;https://github.com/mitmath/1806/blob/master/summaries.md&quot;&gt;mitmath_1806_ 18.06 course at MIT - Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MIT课程的中文笔记：&lt;a href=&quot;https://github.com/ML-NLPChina/MIT-Linear-Algebra-Notes&quot;&gt;MIT-Linear-Algebra-Notes - Github&lt;/a&gt;，作者：丁坤博、覃立波&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联系方式  ：&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;mailto:TonyYin@88.com&quot;&gt;TonyYin@88.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>【2021牛客OI赛前集训营】树数树</title><link>https://www.tonyyin0418.com/blog/oi-solution/nowcoder2021-day2-c</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/nowcoder2021-day2-c</guid><description>算法题解</description><pubDate>Thu, 07 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目来源：&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/20107/C&quot;&gt;2021牛客OI赛前集训营-提高组（第二场）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个点的&lt;strong&gt;有根树&lt;/strong&gt;，根为 $1$。&lt;/p&gt;
&lt;p&gt;长度为 $m$ 的序列 $a$ 满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\forall i\in(1, m]$，$a_i$ 为 $a_{i-1}$ 的祖先或 $a_{i-1}$ 是 $a_{i}$ 的&lt;strong&gt;祖先&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;$\forall 1\leq i &amp;#x3C; j\leq m$，$a_i\neq a_j$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;求&lt;strong&gt;最长的&lt;/strong&gt;序列 $a$ 的&lt;strong&gt;长度&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;h3&gt;$\rm{40\sim 50pts}$&lt;/h3&gt;
&lt;p&gt;大样例提示很明显。&lt;/p&gt;
&lt;p&gt;对任意的二叉树（包括链），答案均为节点个数；对菊花图，答案为 $3$。&lt;/p&gt;
&lt;h3&gt;$\rm{100pts}$&lt;/h3&gt;
&lt;p&gt;上面的部分对正解提示也很明显。&lt;/p&gt;
&lt;p&gt;容易发现，选序列的策略是：对于一个点 $u$，先选一个它的子树处理，再选其自己，再选另一个子树处理。&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;每次处理完之后，向上合并堆。（用可并堆或&lt;code&gt;multiset&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/10/07/0870c1a220ff3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图序列 $a$ 的选取方法如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/10/07/31b0a44a0c97f.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;被同一个节点选取的两个子树，其根节点颜色相同。无填充色即不选取。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#include&amp;#x3C;ext/pb_ds/priority_queue.hpp&gt;
using namespace std;
using namespace __gnu_pbds;
const int MAXN = 100005;
int T, n;
vector&amp;#x3C;int&gt; G[MAXN];
__gnu_pbds::priority_queue&amp;#x3C;int,less&amp;#x3C;int&gt;,pairing_heap_tag&gt;q[MAXN];
void dfs(int u,int father) {
    for(int v:G[u]) {
        if(v == father) continue;
        dfs(v, u);
        q[u].join(q[v]);
        q[v].clear();
    }
    int x = 1;
    for(int i = 1; i &amp;#x3C;= 2; i++)
    {
        if(q[u].empty()) break;
        x += q[u].top(); q[u].pop();
    }
    q[u].push(x);
    return;
}
void solve() {
   	scanf(&quot;%d&quot;, &amp;#x26;n);
    for(int i = 1; i &amp;#x3C; n; i++) {
        int x, y;
        scanf(&quot;%d%d&quot;, &amp;#x26;x, &amp;#x26;y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    dfs(1,0);
    printf(&quot;%d\n&quot;, q[1].top());
    for(int i = 1; i &amp;#x3C;= n; i++) {
        G[i].clear();
		q[i].clear();
	}
    return;
}
int main() {
    scanf(&quot;%d&quot;, &amp;#x26;T);
    while(T--) solve();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【2021牛客OI赛前集训营】与巨</title><link>https://www.tonyyin0418.com/blog/oi-solution/nowcoder2021-day1-c</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/nowcoder2021-day1-c</guid><description>算法题解</description><pubDate>Tue, 05 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目来源：&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/20106&quot;&gt;2021牛客OI赛前集训营-提高组（第一场）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;定义序列 $f$：
$$
f_i=
\left{
\begin{array}{lr}
1,&amp;#x26;i=1\
f_{i-1}\times 2+1, &amp;#x26;i&gt;1
\end{array}
\right.
$$
定义函数 $G(x)=\min\limits_{f_i\geq x}(f_i)$.&lt;/p&gt;
&lt;p&gt;定义序列 $\operatorname{dp}$：
$$
\operatorname{dp}&lt;em&gt;i=
\left{
\begin{array}{lr}
0,&amp;#x26;i=0\
\max\left{
\begin{array}{lr}
\operatorname{dp}&lt;/em&gt;{i-1}\
[(i\times c)&amp;#x26;G(i)==i]\times i
\end{array}
\right}, &amp;#x26;i&gt;0
\end{array}
\right.
$$
求 $\sum_{i=0}^n{\operatorname{dp}_i}\bmod 998244353$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;直接暴力有 $\rm{20pts}$.&lt;/p&gt;
&lt;p&gt;观察 $f_i$ 的形式，$G(i)$ 一定可以表示为 $2^{t+1}-1,t\in \Bbb{N^*}$.&lt;/p&gt;
&lt;p&gt;把二进制形式写出来，$x&amp;#x26;G(i)$ 等价于 $x\bmod 2^{t+1}$.&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;当 $i\in[2^t, 2^{t+1}-1]$ 时&lt;/strong&gt;：
$$
\begin{array}{ll}
&amp;#x26; (i\times c) &amp;#x26; G(i)=i\
\Rightarrow &amp;#x26; (i\times c)\bmod 2^{t+1}=i\
\Rightarrow &amp;#x26; i\times (c-1)\equiv0\pmod{2^{t+1}}
\end{array}
$$
把 $(c-1)$ 中二的幂次提出来，设 $c-1=2^p\times x$，则 $i$ 需要含有因子 $2^{t+1-p}$，即 $i\equiv 0\pmod{2^{t+1-p}}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 有特殊情况，若 $c-1=0$，条件恒成立，答案直接就是 $\sum_{i=0}^n{i}$，需要单独判断。&lt;/p&gt;
&lt;p&gt;设 $m$ 为二进制下 $n$ 的长度 $|n|$.&lt;/p&gt;
&lt;p&gt;考虑枚举上面的 $t$，由于 $m$ 位二进制数的取值范围是 $[0, 2^m-1]$，而 $G(i)=2^{t+1}-1$，因此 $t\in[0, m-1]$。&lt;/p&gt;
&lt;p&gt;分两种情况讨论。&lt;/p&gt;
&lt;h4&gt;当 $t\neq m-1$ 时&lt;/h4&gt;
&lt;p&gt;$[2^t, 2^{t+1}-1]$ 内的数，最高位都为 $t$.&lt;/p&gt;
&lt;p&gt;$i$ 需要含有因子 $2^{t+1-p}$，为了方便表示，设 $g=t+1-p$.&lt;/p&gt;
&lt;p&gt;$i$ 含有 $2^g$，这样的数为：
$$
2^t,\quad 2^t+2^g,\quad  2^t+2\cdot 2^g,\quad  2^t+3\cdot 2^g,\cdots,\quad 2^t+x\cdot 2^g
$$
$x$ 就是最后一项的系数，满足 $2^t+(x+1)\cdot2^g=2^{t+1}$.&lt;/p&gt;
&lt;p&gt;上面共有 $x+1$ 项，是等差数列，可以直接求和。&lt;/p&gt;
&lt;p&gt;依题意，每个数对答案有 $2^g$ 次贡献，还要对 $\operatorname{sum}$ 乘上 $2^g$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 当 $g=t+1$，即 $c\bmod 2=0$ 时，上面的等差数列不存在，答案直接为 $0$。为了方便实现，可以在代码的最前面特判这种情况。&lt;/p&gt;
&lt;h4&gt;当 $t=m-1$ 时&lt;/h4&gt;
&lt;p&gt;和上面类似。不过由于 $2^{t+1}&gt;n$，在 $[2^t, 2^{t+1}-1]$ 当中有一部分数比 $n$ 大，因此 $x$ 的计算方法需要改变。&lt;/p&gt;
&lt;p&gt;具体地，我们需要满足 $2^t+x\times 2^g\leq n$，因此，$x=\lfloor\frac{n}{2^g}\rfloor-2^{t-g}$.&lt;/p&gt;
&lt;p&gt;同样地，对这个 $x+1$ 项的等差数列求和，乘上 $2^g$ 加入贡献。&lt;/p&gt;
&lt;p&gt;在这个计算方法中，等差数列的最后一项也被计数了 $2^g$ 次，认为其对 $[2^t+x\cdot 2^g, 2^t+(x+1)\cdot 2^g-1]$ 这个范围内都有贡献。&lt;/p&gt;
&lt;p&gt;然而区间右端点可能 $&gt;n$，所以要减去 $2^t+(x+1)\cdot 2^g-1-n$ 次贡献，每次贡献的值是 $2^t+x\times 2^g$。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;注意等差数列求和的时候，要用 $2$ 的逆元代替除法.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXLEN = 1e7 + 10, mod = 998244353, inv2 = 499122177;
int T, m, c;
char s[MAXLEN];
int pow_2[MAXLEN];
inline int calc(int st, int ed, int x) {//首项、末项、项数
	if(x &amp;#x3C;= 0) return 0;
	return (st + ed) * inv2 % mod * x % mod;
}
void solve_0() {
	int n = 0;
	for(int i = 1; i &amp;#x3C;= m; i++) n = ((n &amp;#x3C;&amp;#x3C; 1) + s[i] - &apos;0&apos;) % mod;
	printf(&quot;%lld\n&quot;, n * (n + 1) % mod * inv2 % mod);
}
signed main() {
	pow_2[0] = 1;
	for(int i = 1; i &amp;#x3C;= 1e7; i++)
		pow_2[i] = (pow_2[i - 1] &amp;#x3C;&amp;#x3C; 1) % mod;
	scanf(&quot;%lld&quot;, &amp;#x26;T);
	while(T--) {
		scanf(&quot;%s%lld&quot;, (s+1), &amp;#x26;c);
		if(c % 2 == 0) {
			printf(&quot;0\n&quot;);
			continue;
		}
		if(c - 1 == 0) {
			solve_0();
			continue;
		}
		m = strlen(s + 1); c = c - 1;
		int p = 0, ans = 0;
		while(c != 0 &amp;#x26;&amp;#x26; c % 2 == 0) p++, c /= 2;
		for(int t = 0; t &amp;#x3C;= m - 1; t++) {
			int g = max(0ll, t + 1 - p);
			//c%2==0的情况在上面处理过了，所以可以像下面这样对等差数列求和
			if(t &amp;#x3C; m - 1) { //t &amp;#x3C; m - 1时
				ans += pow_2[g] * calc(pow_2[t], pow_2[t+1]-pow_2[g], pow_2[t+1-g]-pow_2[t-g]) % mod;
				ans %= mod;
			} else { //t = m - 1时
				//n/(2^g)下取整，也就是二进制下左边的前n-g位的值
				int x = 0;
				for(int i = 1; i &amp;#x3C;= m - g; i++)
					x = ((x &amp;#x3C;&amp;#x3C; 1) + s[i] - &apos;0&apos;) % mod;
				x = (x + mod - pow_2[t - g]);
				ans += pow_2[g] * calc(pow_2[t], (pow_2[t] + x * pow_2[g]) % mod, x + 1);
				ans %= mod;
				int n = 0, r = (pow_2[t] + (x + 1) * pow_2[g] % mod - 1) % mod;
				for(int i = 1; i &amp;#x3C;= m; i++)
					n = ((n &amp;#x3C;&amp;#x3C; 1) + s[i] - &apos;0&apos;) % mod;
				ans -= (r - n) * (pow_2[t] + x * pow_2[g] % mod) % mod;
				ans %= mod;
			}
		}
		ans = (ans + mod) % mod;
		printf(&quot;%lld\n&quot;, ans);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【ZROI–21暑期AB组-Day3】OSU</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi1922</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi1922</guid><description>不等式好题</description><pubDate>Tue, 05 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;见：&lt;a href=&quot;http://zhengruioi.com/problem/1922&quot;&gt;21暑期AB组高端峰会 - Day3 - OSU - Zhengrui Online Judge&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;或：&lt;a href=&quot;https://www.luogu.com.cn/problem/T207786&quot;&gt;T207786 [ZROI#1922] OSU - 洛谷 - 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;柯西不等式：
$$
\sum_{i=1}^{n}{{a_i}^2}\sum_{i=1}^{n}{{b_i}^2}\geq (\sum_{i=1}^{n}{a_ib_i})^2
$$
题目想要：
$$
\sum_{i=1}^{t}{p_ic^{i-1}}= w_0
$$
利用柯西不等式：
$$
\sum_{i=1}^{t}{{p_i}^2}\sum_{i=1}^{t}{c^{2(i-1)}}\geq (\sum_{i=1}^{t}{p_ic^{i-1}})^2=  {w_0}^2
$$
移项得到：
$$
\sum_{i=1}^{t}{{p_i}^2}\geq \frac{{w_0}^2}{\sum_{i=1}^{t}{c^{2(i-1)}}}
$$
不等式左侧即为答案，取最小值：
$$
\operatorname{Ans}=\frac{{w_0}^2}{\sum_{i=1}^{t}{c^{2(i-1)}}}
$$
可以 $\mathcal{O}(t)$ 计算。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P7887】「MCOI-06」Existence of Truth</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7887</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7887</guid><description>算法题解</description><pubDate>Fri, 01 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目来源&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/contest/52020&quot;&gt;【LGR-093】洛谷 10 月月赛 I &amp;#x26; MCOI R6 Div.1&lt;/a&gt; A题&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/contest/52021&quot;&gt;【LGR-093】洛谷 10 月月赛 I &amp;#x26; MCOI R6 Div.2&lt;/a&gt; C题&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;可能存在一个非负整数数序列 $a_1,a_2,\dots,a_n$ 使得 $0\leq a_i &amp;#x3C; 10^9+7$。&lt;/p&gt;
&lt;p&gt;给定 $x_1,x_2,\cdots,x_n$，$y_1,y_2,\cdots,y_n$，$z_1,z_2,\cdots,z_n$，已知对于 $1\le i\le n$ 满足：
$$
x_i\left(\sum_{j=1}^ia_j\right)+y_i\left(\sum_{j=i}^na_j\right)\equiv z_i\pmod{10^9+7}
$$
先求满足条件的解的个数。若只有一组解，求出 $a_1,a_2,\dots,a_n$。&lt;/p&gt;
&lt;p&gt;有多测。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 $10%$ 的数据，$n=1$，&lt;/p&gt;
&lt;p&gt;对于另外 $19%$ 的数据，$n\leq 100$，&lt;/p&gt;
&lt;p&gt;对于另外 $19%$ 的数据，$x_i=y_i=1$，&lt;/p&gt;
&lt;p&gt;对于另外 $22%$ 的数据，保证有唯一解。&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\leq n\leq 2\times 10^5$，$1\leq x_i, y_i\leq 10^9+7$，$0\leq z_i\leq 10^9+7$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;$\rm{Subtask}$ $1$&lt;/h2&gt;
&lt;p&gt;对于 $10%$ 的数据，$n=1$.&lt;/p&gt;
&lt;p&gt;按照题意写出关于 $a_1$ 的方程：
$$
x_1a_1+y_1a_1\equiv z_1 \pmod{10^9+7}
$$
即：
$$
(x_1+y_1)a_1\equiv z_1\pmod{10^9+7}
$$
注意到此时 $a_1$ &lt;strong&gt;不一定有唯一解&lt;/strong&gt;，需要分类讨论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一、&lt;/strong&gt; 当 $x_1+y_1=0$ 且 $z_1\neq 0$ 时，方程无解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、&lt;/strong&gt; 当 $x_1+y_1=0$ 且 $z_1=0$ 时，$a_1$ 可以在 $[0, 10^9+7)$ 上任取，共有 $10^9+7$ 组解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三、&lt;/strong&gt; 对于其他情况均有唯一解：
$$
a_1=\frac{z_1}{x_1+y_1}\pmod{10^9+7}
$$
用乘法逆元处理即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if(n == 1) cout &amp;#x3C;&amp;#x3C; 1 &amp;#x3C;&amp;#x3C; endl &amp;#x3C;&amp;#x3C; z[1] * inv((x[1] + y[1]) % mod) % mod &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里如果只考虑情况三，也可以拿到这 $10$ 分。&lt;/p&gt;
&lt;p&gt;但由于题目有多组测试数据，如果后续在代码中保留对 $n=1$ 的特判，不分类讨论会被卡掉。&lt;/p&gt;
&lt;p&gt;然后还查不出来）&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $3$&lt;/h2&gt;
&lt;p&gt;对于另外 $19 %$ 的数据，$x_i=y_i=1$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为了方便表示，后面涉及的所有运算都在 $\bmod 10^9+7$ 意义下进行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把 $n$ 个方程列出：
$$
\left {
\begin{aligned}
x_1(a_1)+y_1(a_1+a_2+\cdots+a_n)=z_1\
x_2(a_1+a_2)+y_2(a_2+\cdots+a_n)=z_2\
x_3(a_1+\cdots+a_3)+y_3(a_3+\cdots+a_n)=z_3\
\cdots\
x_n(a_1+a_2+\cdots+a_n)+y_n(a_n)=z_n\
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;p&gt;若 $x_i=y_i=1$，那么：&lt;/p&gt;
&lt;p&gt;$$
\left{
\begin{aligned}
(a_1+a_2+\cdots+a_n)+a_1=z_1 &amp;#x26;(1)\
(a_1+a_2+\cdots+a_n)+a_2=z_2 &amp;#x26;(2)\
(a_1+a_2+\cdots+a_n)+a_3=z_3 &amp;#x26;(3)\
\cdots&amp;#x26;\cdots\
(a_1+a_2+\cdots+a_n)+a_n=z_n &amp;#x26;(n)\
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;p&gt;容易想到把 $n$ 个方程相加，
$$
(n+1)(a_1+a_2+\cdots+a_n)=z_1+z_2+\cdots+z_n
$$
把 $n+1$ 除过去就能得到 $a_1+a_2+\cdots+a_n$ 的值，再代入 $(1)\cdots(n)$ 式即可求出答案。&lt;/p&gt;
&lt;p&gt;同样的，需要分类讨论一下解的情况。但对于这部分数据，不讨论也能通过。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if(subtask3) {
    int sumz = 0;
    for(int i = 1; i &amp;#x3C;= n; i++) (sumz += z[i]) %= mod;
    int sum = sumz * inv(n + 1) % mod;
    cout &amp;#x3C;&amp;#x3C; 1 &amp;#x3C;&amp;#x3C; endl;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        cout &amp;#x3C;&amp;#x3C; (((z[i] - sum) % mod) + mod) % mod &amp;#x3C;&amp;#x3C; &quot; &quot;;
    } cout &amp;#x3C;&amp;#x3C; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;$\rm{Subtask}$ $2$&lt;/h2&gt;
&lt;p&gt;对于另外 $19%$ 的数据，$n\leq 100$。
$$
\left{
\begin{array}{l}
x_1(a_1)+y_1(a_1+a_2+\cdots+a_n)=z_1\
x_2(a_1+a_2)+y_2(a_2+\cdots+a_n)=z_2\
x_3(a_1+\cdots+a_3)+y_3(a_3+\cdots+a_n)=z_3\
\cdots\
x_n(a_1+a_2+\cdots+a_n)+y_n(a_n)=z_n\
\end{array}
\right.
$$
对于这个方程组，我们可以把每个方程进行整理，转化为这样的形式：
$$
\left{
\begin{array}{l}
c_{1,1}a_1+c_{1,2}a_2+\cdots+c_{1,n}a_n=z_1\
c_{2,1}a_1+c_{2,2}a_2+\cdots+c_{2,n}a_n=z_2\
\cdots\
c_{n,1}a_1+c_{n,2}a_2+\cdots+c_{n,n}a_n=z_n
\end{array}
\right.
$$
也就是把每个方程中，未知数的系数求出来。&lt;/p&gt;
&lt;p&gt;之后使用&lt;a href=&quot;https://www.tonyyin.top/2021/09/gauss-elimination/&quot;&gt;&lt;strong&gt;高斯消元&lt;/strong&gt;&lt;/a&gt;直接解方程。&lt;/p&gt;
&lt;p&gt;过程中，若存在自由元，则为解的个数为 $10^9+7$，不管有几个自由元都是。不知道为什么。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;若方程组通过化简可以得到 $0\cdot x=0$，那么 $x$ 的取值任意，称 $x$ 为自由元。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n^3)$.&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $4$&lt;/h2&gt;
&lt;p&gt;对于另外 $22%$ 的数据，保证有唯一解。&lt;/p&gt;
&lt;p&gt;题目中的方程显然不同于随机生成的方程，要找到一些可以利用的特性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有可能&lt;/strong&gt;可以发现，方程与区间和密切相关，于是考虑前缀和。&lt;/p&gt;
&lt;p&gt;设 $s_i=\sum_{j=1}^ja_j$，则方程组通项为：
$$
x_i\cdot s_i+y_i(s_n-s_{j-1})=z_i\quad (i)
$$
也就是：
$$
-y_i\cdot s_{j-1} + x_i\cdot s_i+y_i\cdot s_n=z_i\quad (i)
$$
这种形式下，每个方程涉及的未知数更少，更容易处理。&lt;/p&gt;
&lt;p&gt;考虑系数矩阵的形态：
$$
\begin{matrix}
&amp;#x26;&amp;#x26;\textcolor{gray}{1} &amp;#x26;\textcolor{gray}{2} &amp;#x26;\textcolor{gray}{3} &amp;#x26;\textcolor{gray}{4} &amp;#x26;\textcolor{gray}{5} &amp;#x26;\textcolor{gray}{6}\\
\textcolor{gray}{1} &amp;#x26;&amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{2} &amp;#x26;&amp;#x26;x &amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{3} &amp;#x26;&amp;#x26;  &amp;#x26;x &amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{4} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;x &amp;#x26;x &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{5} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x &amp;#x26;x &amp;#x26;x \
\textcolor{gray}{6} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x &amp;#x26;x \
\end{matrix}
$$
$x$ 表示系数不为 $0$。&lt;/p&gt;
&lt;p&gt;这样第 $1$ 行只需要消第 $2$ 行，第 $2$ 行消第 $3$ 行，以此类推，最后的矩阵形如：
$$
\begin{matrix}
&amp;#x26;&amp;#x26;\textcolor{gray}{1} &amp;#x26;\textcolor{gray}{2} &amp;#x26;\textcolor{gray}{3} &amp;#x26;\textcolor{gray}{4} &amp;#x26;\textcolor{gray}{5} &amp;#x26;\textcolor{gray}{6}\\
\textcolor{gray}{1} &amp;#x26;&amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{2} &amp;#x26;&amp;#x26;0 &amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{3} &amp;#x26;&amp;#x26;  &amp;#x26;0 &amp;#x26;x &amp;#x26;  &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{4} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;0 &amp;#x26;x &amp;#x26;  &amp;#x26;x \
\textcolor{gray}{5} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;0 &amp;#x26;x &amp;#x26;x \
\textcolor{gray}{6} &amp;#x26;&amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;  &amp;#x26;0 &amp;#x26;x \
\end{matrix}
$$
于是 $a_n$ 可求，分别代入第 $1\sim n-1$ 个方程即可求解。&lt;/p&gt;
&lt;p&gt;这样的时间复杂度是 $\mathcal{O}(n\log n)$，$\log n$ 来自求逆元。&lt;/p&gt;
&lt;p&gt;对于空间，每行只有 $3$ 个系数需要存，不用开矩阵，也是 $\mathcal{O}(n)$ 的。&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $5$&lt;/h2&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\leq n\leq 2\times 10^5$，$1\leq x_i, y_i\leq 10^9+7$，$0\leq z_i\leq 10^9+7$.&lt;/p&gt;
&lt;p&gt;上面的时空复杂度已经可以通过此题，只需要多加一个判断解的个数。&lt;/p&gt;
&lt;p&gt;判断的方法在 $\rm{Subtask}$ $1$ 中提到过了，但&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;code&gt;read()&lt;/code&gt; 和头文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define int long long
const int MAXN = 2e5 + 10, mod = 1e9 + 7;
inline int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		x = x * x % mod;
		k &gt;&gt;= 1;
	}
	return ret;
}
inline int inv(int x) {return power(x, mod - 2);}
int T, n;
int x[MAXN], y[MAXN], z[MAXN];
int a[MAXN], b[MAXN], c[MAXN];
int s[MAXN];
signed main() {
	T = read();
	while(T--) {
		n = read();
		for(int i = 1; i &amp;#x3C;= n; i++) {
			x[i] = read(); y[i] = read(); z[i] = read();
		}
		//对每个x,y,z，x[i]s[i] - y[i]s[i-1] + y[i]s[n] = z[i]
		for(int i = 1; i &amp;#x3C; n; i++) {//高斯消元解前缀和
			a[i] = mod - y[i]; b[i] = x[i]; c[i] = y[i];
		} a[n] = mod - y[n]; c[n] = (x[n] + y[n]) % mod;
		for(int i = 1; i &amp;#x3C; n; i++) {//用第i行消掉第i+1行的第i列
			int k = a[i + 1] * inv(b[i]) % mod;
			(a[i + 1] -= k * b[i] % mod) %= mod;
			(c[i + 1] -= k * c[i] % mod) %= mod;
			(z[i + 1] -= k * z[i] % mod) %= mod;
		}
		int roots = 1;
		if(c[n] == 0 &amp;#x26;&amp;#x26; z[n] != 0) {
            putchar(&apos;0&apos;); putchar(&apos;\n&apos;);
            continue;
        }
		else if(c[n] == 0 &amp;#x26;&amp;#x26; z[n] == 0) roots = mod;
		else s[n] = z[n] * inv(c[n]) % mod;
		for(int i = 1; i &amp;#x3C;= n - 1; i++) {
			int right = (z[i] - c[i] * s[n] % mod) % mod;
			if(b[i] == 0 &amp;#x26;&amp;#x26; right != 0) {roots = 0; break;}
			else if(b[i] == 0 &amp;#x26;&amp;#x26; right == 0) roots += mod;
			else s[i] = ((z[i] - c[i] * s[n] % mod) * inv(b[i]) % mod + mod) % mod;
		}
		if(roots != 1) {printf(&quot;%lld\n&quot;, roots); continue;}
		else {putchar(&apos;1&apos;); putchar(&apos;\n&apos;);}
		for(int i = 1; i &amp;#x3C;= n; i++) 
            printf(&quot;%lld &quot;, ((s[i] - s[i - 1]) % mod + mod) % mod);
        putchar(&apos;\n&apos;);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P7116】 [NOIP2020] 微信步数</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7116</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7116</guid><description>算法题解</description><pubDate>Tue, 14 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P7116&quot;&gt;P7116 [NOIP2020] 微信步数 - 洛谷 - 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定 $k$ 维场地，场地中的坐标表示为 $(a_1, a_2, \cdots, a_k)$，场地有大小限制 $w$，$1\leq a_i\leq w_i$.&lt;/p&gt;
&lt;p&gt;给定 $n$ 个指令构成的指令序列，每个指令用 $(c_i, d_i)$ 表示，代表在第 $c_i$ 维上移动 $d_i\in{-1, 1}$。形式化地，若当前位于 $(a_1, a_2, \cdots, a_{c_i}, \cdots, a_k)$，下一步会到 $(a_1, a_2, \cdots, a_{c_i}+d_i, \cdots, a_k)$.&lt;/p&gt;
&lt;p&gt;现在，小 $C$ 要从场地中的所有 $P=\prod_{i=1}^k{w_i}$ 个点分别出发，不断重复这个路线，直到其走出场地范围。也就是说，走完 $n$ 步之后，如果小 $C$ 还在场地内，他将回到第 $1$ 步，从头再走一遍。&lt;/p&gt;
&lt;p&gt;每执行一次指令代表走一步，求从这 $P$ 个点出发一共走了多少步，对 $10^9+7$ 取模。&lt;/p&gt;
&lt;p&gt;若有一天永远走不出场地，输出 $-1$.&lt;/p&gt;
&lt;p&gt;| 测试点编号 | $n \le$ | $k \le$ | $w_i \le$ |
|:-:|:-:|:-:|:-:|
| $1 \sim 3$ | $5$ | $5$ | $3$ |
| $4 \sim 6$ | $100$ | $3$ | $10$ |
| $7 \sim 8$ | ${10}^5$ | $1$ | ${10}^5$ |
| $9 \sim 12$ | ${10}^5$ | $2$ | ${10}^6$ |
| $13 \sim 16$ | $5 \times {10}^5$ | $10$ | ${10}^6$ |
| $17 \sim 20$ | $5 \times {10}^5$ | $3$ | ${10}^9$ |&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h4&gt;分析&lt;/h4&gt;
&lt;p&gt;如果分别计算每个点的话，那复杂度一定会有一个 $\prod_{i=1}^k{w_i}$，所以把每一维分开考虑。&lt;/p&gt;
&lt;p&gt;不妨设我们在考虑第 $i$ 维的情况。对于维度 $i$ 的某个位置 $p\in[1, w_i]$，设 $\operatorname{time}_i(p)$ 表示在仅考虑维度 $i$ 时小 C 走出场地需要的最少步数。如果无论如何都无法走出场地那么 $\operatorname{time}_i(p)=+\infty$ 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;这里是仅考虑维度 $i$ 时，不考虑先从其他维度走出场地的情况。&lt;/p&gt;
&lt;p&gt;求出 $\operatorname{time}$ 这个函数之后，小 C 从 $(a_1,a_2,\dots,a_k)$ 走出场地需要的步数就是 $\min_{i=1}^k\operatorname{time}_i(a_i)$.&lt;/p&gt;
&lt;p&gt;因此，题目最终的答案表示为：
$$
\operatorname{Ans} = \sum_{(a_1, a_2\cdots, a_k)\in \operatorname{Map}}\min_{i=1}^k\operatorname{time}_{i}(a_i)
$$&lt;/p&gt;
&lt;h4&gt;转化&lt;/h4&gt;
&lt;p&gt;这样求解的复杂度仍是 $\prod_{i=1}^k{w_i}$ 的，不能接受。对上面的式子改变枚举顺序，可以得到：
$$
\operatorname{Ans} = \sum_{(a_1, a_2\cdots, a_k)\in \operatorname{Map}}\min_{i=1}^k\operatorname{time}&lt;em&gt;{i}(a_i)=\sum&lt;/em&gt;{i\ge 1}\prod_{j=1}^k(\sum_{t=1}^{w_j}[\operatorname{time}&lt;em&gt;j(t)\ge i])
$$
$\sum\limits&lt;/em&gt;{t=1}^{w_j}[\operatorname{time}_j(t)\ge i]$ 代表，在仅考虑第 $j$ 维的情况下，&lt;strong&gt;第 $j$ 维中走完 $i$ 步还在地图内的坐标个数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$\prod\limits_{j=1}^k(\sum\limits_{t=1}^{w_j}[\operatorname{time}_j(t)\ge i])$ 代表，&lt;strong&gt;走完 $i$ 步仍然在场地内的点的个数&lt;/strong&gt;。根据乘法原理，把每个维度上可行的坐标乘起来，所得到的就是可行点的个数。&lt;/p&gt;
&lt;p&gt;最外层枚举 $i$ 表示：&lt;strong&gt;走完 $i$ 步仍然在场地内&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;显然，枚举 $i$，把所有可行点数加起来，得到的就是总步数。&lt;/p&gt;
&lt;p&gt;容易发现，对这个式子求和的复杂度不再需要 $\prod_{i=1}^k{w_i}$ 了，下面是一种可行的计算方法。&lt;/p&gt;
&lt;h4&gt;计算方法&lt;/h4&gt;
&lt;p&gt;假设我们现在已经求出了所有的 $\operatorname{time}_i(j)$.&lt;/p&gt;
&lt;p&gt;首先，我们把所有的 $\operatorname{time}_i(j)$ 拿出来，降序排序，同时存储 $\operatorname{time}_i(j)$ 所处的维度。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= k; i++) 
    for(int j = 1; j &amp;#x3C;= w[i]; j++) 
        T[++cnt] = make_pair(Time[i][j], i);
sort(T + 1, T + cnt + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，我们按降序遍历 &lt;code&gt;T[]&lt;/code&gt;，对每维开一个桶，记录当前属于该维度的 $\operatorname{time}_i(j)$ 的个数。&lt;/p&gt;
&lt;p&gt;当我们枚举到 $T[i]\neq T[i-1]$ 时，此时所有 $\geq T[i]$ 的 $\operatorname{time}_i(j)$ 已经全部加入到了桶中。&lt;/p&gt;
&lt;p&gt;把桶上的数累乘，$\prod_{j=1}^k{\operatorname{sum}&lt;em&gt;j}=\prod&lt;/em&gt;{j=1}^k(\sum_{t=1}^{w_j}[\operatorname{time}_j(t)\ge T_i])$，就是上面表达式中 $i=T_i$ 时的值，这很显然。&lt;/p&gt;
&lt;p&gt;对应到上面 $\operatorname{Ans}$ 的表达式，$i\in[T_{i-1} + 1, T_i]$ 这个范围中的 $\prod_{j=1}^k(\sum_{t=1}^{w_j}[\operatorname{time}_j(t)\ge i])$ 都是相同的。&lt;/p&gt;
&lt;p&gt;所以 $\operatorname{Ans} += (T_i - T_{i-1})\times \prod_{j=1}^k{\operatorname{sum}_j}$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;pair&amp;#x3C;int, int&gt; T[10000010]; //first存Time，second存维度
int sum[MAXK]; //sum[i]代表：维度i下，可选的点的个数
int cnt = 0, ans = 0;
for(int i = cnt; i &gt;= 1; i--) {
	sum[T[i].second]++;
	if(T[i - 1].first != T[i].first) { // &gt;=T[i]的部分已经加入完成
		ll tmp = 1;
		for(int j = 1; j &amp;#x3C;= k; j++) tmp *= sum[j];
		ans += (T[i].first - T[i - 1].first) * tmp;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码没加取模，时间复杂度 $\mathcal{O}(k\cdot \sum w_i)$.&lt;/p&gt;
&lt;h4&gt;求 $\operatorname{time}_i(j)$&lt;/h4&gt;
&lt;p&gt;对每个维度分别求。&lt;/p&gt;
&lt;p&gt;设 $\operatorname{step}_{i+n}$ 表示在当前枚举的维度上，&lt;strong&gt;在一轮内，移动 $i$ 的距离需要的最小步数&lt;/strong&gt;。由于 $i$ 不一定非负，所以下表要整体加 $n$.&lt;/p&gt;
&lt;p&gt;直接设初始在位置 $0$，遍历指令序列即可求出 $\operatorname{step}$，也能求出&lt;strong&gt;遍历一遍序列后移动的距离&lt;/strong&gt; $\operatorname{cur}$.&lt;/p&gt;
&lt;p&gt;如果&lt;strong&gt;在一个周期内&lt;/strong&gt;，$j$ 就可以走出场地，那么此时 $\operatorname{time}_i(j)$ 已经可以求出了。&lt;/p&gt;
&lt;p&gt;如果 $j$ &lt;strong&gt;需要多个周期&lt;/strong&gt;才能出去，我们考虑&lt;strong&gt;先求出这个周期数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不妨设 $j$ 在多个周期后从正方向（$&gt;w_i$）离开场地。&lt;/p&gt;
&lt;p&gt;设 $j$ 经过若干个整周期后，位置为 $x$，则其可以在一个新的周期中走出场地，当且仅当 $w_i-x+1\leq \operatorname{Maxdis}$.&lt;/p&gt;
&lt;p&gt;$\operatorname{Maxdis}$ 代表，在周期内，最多可以向正方向移动多少距离，&lt;strong&gt;这不一定等于周期结束时的移动距离&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，周期数可以表示为：
$$
\operatorname{T}=
\left\lceil
\frac{w_i-j+1}{\operatorname{cur}}
\right\rceil
$$
求出了周期数，就可以直接求值了：
$$
\operatorname{time}_i(j)=\operatorname{T}\times n+\operatorname{step}[(w_i-j+1)-\operatorname{T}\times \operatorname{cur}+n]
$$
$+n$ 的原因前面提到过了。&lt;/p&gt;
&lt;p&gt;对于从负方向 $&amp;#x3C;0$ 离开场地的情况也类似，只需要求出 $\operatorname{Mindis}$ 即可。&lt;/p&gt;
&lt;p&gt;$\operatorname{Maxdis}$ 和 $\operatorname{Mindis}$ 可以在求 $\operatorname{step}$ 的时候顺便处理出来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int step[MAXN &amp;#x3C;&amp;#x3C; 1];
int Time[MAXK][MAXWI];
for(int i = 1; i &amp;#x3C;= k; i++) {//第i维
    memset(step, 0x3f, sizeof(step)); step[n] = 0;
    int cur = 0, maxdis = 0, mindis = 0;
    for(int j = 1; j &amp;#x3C;= n; j++) if(c[j] == i) {
        cur += d[j];
        if(cur &gt; maxdis) maxdis = cur;
        if(cur &amp;#x3C; mindis) mindis = cur;
        step[cur + n] = min(step[cur + n], j);
    }
    //cur: 走n步，一个周期能移动cur
    for(int j = 1; j &amp;#x3C;= w[i]; j++) {
        //一个周期就能走到的情况
        if(w[i] - j + 1 + n &gt;= 0 &amp;#x26;&amp;#x26; w[i] - j + 1 &amp;#x3C;= n) 
            Time[i][j] = min(Time[i][j], step[w[i] - j + 1 + n]);
        if(n - j &gt;= 0) Time[i][j] = min(Time[i][j], step[-j + n]);
        int rest = 0, tim = 0, T;
        //多个周期才能走到的情况
        if(cur &gt; 0) { //向正方向
            rest = w[i] - j + 1;
            T = max(0ll, (rest - maxdis - 1) / cur + 1); //上取整
            tim = T * n + step[rest - T * cur + n];
            Time[i][j] = min(Time[i][j], tim);
        }
        if(cur &amp;#x3C; 0) { //向负方向
            rest = j;
            T = max(0ll, (rest + mindis - 1) / (-cur) + 1); //上取整
            tim = T * n + step[-rest + T * (-cur) + n];
            Time[i][j] = min(Time[i][j], tim);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样实现的时间复杂度同样为：$\mathcal{O}(k\cdot \sum w_i)$.&lt;/p&gt;
&lt;h4&gt;参考资料&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/230249/ti-xie-wei-xin-bu-shuo&quot;&gt;fight for dream - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://paste.ubuntu.com/p/mP6mcZKqPx/&quot;&gt;Ubuntu Pastebin&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;求自然数幂的和&lt;/h2&gt;
&lt;h4&gt;问题&lt;/h4&gt;
&lt;p&gt;给定 $n, k$，求出：
$$
1^k+2^k+\cdots+n^k=\sum_{i=1}^{n}{i^k}\bmod 1000000007
$$
$n\leq 10^9$，$k\leq 10^6$.&lt;/p&gt;
&lt;p&gt;解决这个问题需要用到一些简单的多项式技巧。&lt;/p&gt;
&lt;h4&gt;拉格朗日插值&lt;/h4&gt;
&lt;p&gt;如果给定一个 $k$ 次多项式上的 $k+1$ 个点值，就可以通过拉格朗日差值求出这个多项式的表达式：
$$
f(x)=\sum_{i=1}^{k+1}y_i\prod_{j\neq i}{\frac{x-x_j}{x_i-x_j}}
$$
对于每个点，我们构造一个 $k$ 次多项式 $g_i(x)$，满足：
$$
g_i(x_t)=
\left{
\begin{align}
&amp;#x26;0 &amp;#x26;t=i\
&amp;#x26;y_t &amp;#x26;t\neq i
\end{align}
\right.
$$
显然这样的多项式可以为：
$$
g_i(x) = y_i\prod_{j\neq i}\frac{x-x_j}{x_i-x_j}
$$
我们把这 $k+1$ 个 $g$ 加起来，每个点值只会被加上一次，所以得到：
$$
f(x)=\sum_{i=1}^{k+1}g_i(x)=\sum_{i=1}^{k+1}y_i\prod_{j\neq i}{\frac{x-x_j}{x_i-x_j}}
$$&lt;/p&gt;
&lt;h4&gt;$k$ 次幂和是 $k+1$ 次多项式&lt;/h4&gt;
&lt;p&gt;$f_0(n) =n$.&lt;/p&gt;
&lt;p&gt;$f_1(n) =\dfrac{n(n+1)}{2}$.&lt;/p&gt;
&lt;p&gt;$f_2(n)=\dfrac{n(n+1)(2n+1)}{6}$.&lt;/p&gt;
&lt;p&gt;$f_3(n)=\dfrac{n^2(n+1)^2}{4}$.&lt;/p&gt;
&lt;p&gt;众所周知，$f_k(n)=\sum_{i=1}^{n}{i^k}$，是一个 $k+1$ 次多项式，可以用数学归纳法&lt;a href=&quot;https://www.luogu.com.cn/blog/luckyblock/solution-cf622f&quot;&gt;证明&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;分析&lt;/h4&gt;
&lt;p&gt;回到题目，设 $S_k(n)$ 为题目所求。由上面的性质，$S_k(n)$ 是 $k+1$ 次多项式，需要 $k+2$ 个点值进行构造。&lt;/p&gt;
&lt;p&gt;如果直接选取 $k+1$ 个点，利用拉格朗日插值计算，复杂度为 $\mathcal{O}(k^2)$，无法通过。&lt;/p&gt;
&lt;h4&gt;优化&lt;/h4&gt;
&lt;p&gt;考虑选连续的一段点，进行优化。&lt;/p&gt;
&lt;p&gt;我们选择 $k+2$ 个自变量，为 $[1, k+2]$ 中的整数。也就是说选择的点集为：${(i, S_k(i))\mid i\in \Bbb{N}^{+}, i\leq k+2}$.&lt;/p&gt;
&lt;p&gt;将这些点代入拉格朗日插值公式，得到：
$$
S_k(n)=\sum_{i=1}^{k+2}{S_k(i)\prod_{j\neq i}{\frac{n-j}{i-j}}}
$$
简单变换后得到：
$$
S_k(n)=\sum_{i=1}^{k+2}{S_k(i){\frac{\prod\limits_{j\neq i}(n-j)}{\prod\limits_{j\neq i}(i-j)}}}
$$
之后开始找规律）&lt;/p&gt;
&lt;p&gt;对于累乘项的分子，暴力拆开：
$$
\begin{align}
\prod_{j\neq i}(n-j)&amp;#x26;= n(n-1)\dots (n-(i-1))\times(n-(i+1))\dots(n-(k+2))\
&amp;#x26;= (\prod_{j=1}^{i-1}(n-j))(\prod_{j=i+1}^{k+2}(n-j))
\end{align}
$$
所以我们维护 $(n-j)$ 的前缀积和后缀积，就能直接得到分子的值了。&lt;/p&gt;
&lt;p&gt;对于分母，假设 $i$ 已经固定，那么有：
$$
\begin{aligned}
&amp;#x26;\dfrac{1}{\prod\limits_{i\not ={j}}(i-j)}\ =
&amp;#x26;\dfrac{1}{i(i-1)(i-2) \dots  1 \times(-1) \dots  (k+2-i-1)(k+2-i)}\=&amp;#x26;(-1)^{k+2-i}\dfrac{1}{i!  (k+2-i)!}
\end{aligned}
$$
所以我们预处理阶乘，再求逆元，就可以快速得到分母的值。&lt;/p&gt;
&lt;p&gt;最后，把变换后的式子代入 $S_k(n)$ 的表达式：
$$
S_k(n)=\sum_{i=1}^{k+2}S_k(i)(-1)^{k+2-i}\dfrac{(\prod\limits_{j=1}^{i-1}(n-j))(\prod\limits_{j=i+1}^{k+2}(n-j))}{i!(k+2-i)!}
$$
时间复杂度是 $\mathcal{O}(k\log k)$.&lt;/p&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXK = 1e6 + 10, mod = 1e9 + 7;
int n, k;
int pre[MAXK], suf[MAXK], fac[MAXK];
int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		k &gt;&gt;= 1;
		x = x * x % mod;
	}
	return ret;
}
inline int inv(int x) {
	return power(x, mod - 2);
}
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;k);
	fac[0] = pre[0] = 1;
	for(int i = 1; i &amp;#x3C;= k + 2; i++) {
		pre[i] = pre[i - 1] * (n - i) % mod;
		fac[i] = fac[i - 1] * i % mod;
	}
	suf[k + 3] = 1;
	for(int i = k + 2; i &gt;= 1; i--) suf[i] = suf[i + 1] * (n - i) % mod;
	int tmp = 0, ans = 0;
	for(int i = 1; i &amp;#x3C;= k + 2; i++) {
		tmp = (tmp + power(i, k)) % mod;
		int a = pre[i - 1] * suf[i + 1] % mod;
		int b = fac[i - 1] * fac[k + 2 - i] % mod;
		if((k - i) % 2 == 0) ans = (ans + tmp * a % mod * inv(b) % mod) % mod;
		else ans = (ans - tmp * a % mod * inv(b) % mod) % mod;
	}
	cout &amp;#x3C;&amp;#x3C; (ans + mod) % mod &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;进一步优化&lt;/h4&gt;
&lt;p&gt;上面做法有两个部分的复杂度带 $\log k$，阶乘的逆元可以线性预处理，问题就只剩：如何快速预处理
$$
1^k, 2^k, 3^k, \cdots, k^k
$$
发现 $f(t)=t^k$ 是完全积性函数，于是可以只求所有质数的 $k$ 次幂，然后线性筛。&lt;/p&gt;
&lt;p&gt;这样整体复杂度大约是 $\mathcal{O}(k)$ 的。&lt;/p&gt;
&lt;h4&gt;参考资料&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/luckyblock/solution-cf622f&quot;&gt;地靈殿 - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://henryhuang.blog.luogu.org/solution-cf622f&quot;&gt;HenryHuang 的博客 - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://oi-wiki.com/math/poly/lagrange/&quot;&gt;拉格朗日插值 - OI Wiki&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>【ZROI-21CSP7连测-Day1-T4】解谜</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-21csp-day1-t4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-21csp-day1-t4</guid><description>算法题解</description><pubDate>Mon, 13 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;$n$ 个点，随机构成一颗有根树，求叶子的期望数量。&lt;/p&gt;
&lt;p&gt;$n\leq 10^9$.&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $1$&lt;/h2&gt;
&lt;p&gt;对于 $20%$ 的数据，$n\leq 20$.&lt;/p&gt;
&lt;p&gt;暴力是过不了的，应该有一些 DP 可以通过。&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $2$&lt;/h2&gt;
&lt;p&gt;设 $f_i$ 表示&lt;strong&gt;包含 $i$ 个节点的有根树数量&lt;/strong&gt;，那么根据卡特兰数公式，有 $f_n=\frac{1}{n+1}C_{2n}^{n}$.&lt;/p&gt;
&lt;p&gt;因此当 $n=20$ 时，不同的有根树数量为 $f_{20}=6564120420$，暴力无法通过 $\rm{Subtask}$ $1$.&lt;/p&gt;
&lt;p&gt;设 $g_i$ 表示有 $i$ 个节点的所有不同形态二叉树的&lt;strong&gt;叶子结点数量和&lt;/strong&gt;，那么题目所求即为：$\frac{g_n}{f_n}$.&lt;/p&gt;
&lt;p&gt;先打表，至少可以打到 $n=10$.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/09/13/52aff1b9e454c.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;观察一下，可以发现一个规律：
$$
g_n=f_{n-1}\cdot n
$$
由 $f_n=\frac{1}{n+1}C_{2n}^{n}$，题目所求的答案可以用只含有 $n$ 的式子表示出来：
$$
\operatorname{Ans}
=\frac{g_n}{f_n}
=\frac{f_{n-1}\cdot n}{f_n}
=\frac{\frac{1}{n}\binom{2n-2}{n-1}\cdot n}{\frac{1}{n+1}\binom{2n}{n}}
=\frac{(n+1)\cdot \frac{(2n-2)!}{(n-1)!(n-1)!}\cdot n}{n\cdot \frac{(2n)!}{(n)!(n)!}}
$$
继续化简得到：
$$
\operatorname{Ans}=\frac{(n+1)\cdot \frac{(2n-2)!}{(2n)!}}{\frac{(n-1)!(n-1)!}{(n)!(n)!}}=\frac{(n+1)\cdot \frac{1}{(2n-1)\cdot (2n)}}{\frac{1}{n^2}}=\frac{n\cdot (n+1)}{2\cdot (2n-1)}
$$
用快速幂求逆元即可，时间复杂度为 $\mathcal{O}(\log p)$，$p$ 为模数.&lt;/p&gt;
&lt;h2&gt;证明&lt;/h2&gt;
&lt;p&gt;下面给出 $g_n=f_{n-1}\cdot n$ 的证明。&lt;/p&gt;
&lt;h4&gt;一&lt;/h4&gt;
&lt;p&gt;对于每棵 $n$ 个点的二叉树，如果里面有 $k$ 个叶节点，那么我们分别把这 $k$ 个叶子删去，会得到 $k$ 棵 $n-1$ 个点的二叉树。&lt;/p&gt;
&lt;p&gt;因此，所有 $n$ 个节点的二叉树中，&lt;strong&gt;每个叶子都唯一对应一棵 $n-1$ 个节点的二叉树&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;二&lt;/h4&gt;
&lt;p&gt;对于每棵 $n-1$ 个点的二叉树，有 $n$ 个位置可以接上一个新的叶子节点。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;每棵 $n-1$ 个节点的二叉树都对应 $n$ 个叶子&lt;/strong&gt;。（所有 $n$ 个节点的二叉树的所有叶子中的 $n$ 个）&lt;/p&gt;
&lt;p&gt;综上，结论得证。&lt;/p&gt;</content:encoded></item><item><title>求自然数幂的和 – CF622F</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf622f</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf622f</guid><description>算法题解</description><pubDate>Tue, 31 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;给定 $n, k$，求出：
$$
1^k+2^k+\cdots+n^k=\sum_{i=1}^{n}{i^k}\bmod 1000000007
$$
$n\leq 10^9$，$k\leq 10^6$.&lt;/p&gt;
&lt;p&gt;解决这个问题需要用到一些简单的多项式技巧。&lt;/p&gt;
&lt;h2&gt;拉格朗日插值&lt;/h2&gt;
&lt;p&gt;如果给定一个 $k$ 次多项式上的 $k+1$ 个点值，就可以通过拉格朗日差值求出这个多项式的表达式：
$$
f(x)=\sum_{i=1}^{k+1}y_i\prod_{j\neq i}{\frac{x-x_j}{x_i-x_j}}
$$
对于每个点，我们构造一个 $k$ 次多项式 $g_i(x)$，满足：
$$
g_i(x_t)=
\left{
\begin{align}
&amp;#x26;0 &amp;#x26;t=i\
&amp;#x26;y_t &amp;#x26;t\neq i
\end{align}
\right.
$$
显然这样的多项式可以为：
$$
g_i(x) = y_i\prod_{j\neq i}\frac{x-x_j}{x_i-x_j}
$$
我们把这 $k+1$ 个 $g$ 加起来，每个点值只会被加上一次，所以得到：
$$
f(x)=\sum_{i=1}^{k+1}g_i(x)=\sum_{i=1}^{k+1}y_i\prod_{j\neq i}{\frac{x-x_j}{x_i-x_j}}
$$&lt;/p&gt;
&lt;h2&gt;$k$ 次幂和是 $k+1$ 次多项式&lt;/h2&gt;
&lt;p&gt;$f_0(n) =n$.&lt;/p&gt;
&lt;p&gt;$f_1(n) =\dfrac{n(n+1)}{2}$.&lt;/p&gt;
&lt;p&gt;$f_2(n)=\dfrac{n(n+1)(2n+1)}{6}$.&lt;/p&gt;
&lt;p&gt;$f_3(n)=\dfrac{n^2(n+1)^2}{4}$.&lt;/p&gt;
&lt;p&gt;众所周知，$f_k(n)=\sum_{i=1}^{n}{i^k}$，是一个 $k+1$ 次多项式，可以用数学归纳法&lt;a href=&quot;https://www.luogu.com.cn/blog/luckyblock/solution-cf622f&quot;&gt;证明&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;回到题目，设 $S_k(n)$ 为题目所求。由上面的性质，$S_k(n)$ 是 $k+1$ 次多项式，需要 $k+2$ 个点值进行构造。&lt;/p&gt;
&lt;p&gt;如果直接选取 $k+1$ 个点，利用拉格朗日插值计算，复杂度为 $\mathcal{O}(k^2)$，无法通过。&lt;/p&gt;
&lt;h2&gt;优化&lt;/h2&gt;
&lt;p&gt;考虑选连续的一段点，进行优化。&lt;/p&gt;
&lt;p&gt;我们选择 $k+2$ 个自变量，为 $[1, k+2]$ 中的整数。也就是说选择的点集为：${(i, S_k(i))\mid i\in \Bbb{N}^{+}, i\leq k+2}$.&lt;/p&gt;
&lt;p&gt;将这些点代入拉格朗日插值公式，得到：
$$
S_k(n)=\sum_{i=1}^{k+2}{S_k(i)\prod_{j\neq i}{\frac{n-j}{i-j}}}
$$
简单变换后得到：
$$
S_k(n)=\sum_{i=1}^{k+2}{S_k(i){\frac{\prod\limits_{j\neq i}(n-j)}{\prod\limits_{j\neq i}(i-j)}}}
$$
之后开始找规律）&lt;/p&gt;
&lt;p&gt;对于累乘项的分子，暴力拆开：
$$
\begin{align}
\prod_{j\neq i}(n-j)&amp;#x26;= n(n-1)\dots (n-(i-1))\times(n-(i+1))\dots(n-(k+2))\
&amp;#x26;= (\prod_{j=1}^{i-1}(n-j))(\prod_{j=i+1}^{k+2}(n-j))
\end{align}
$$
所以我们维护 $(n-j)$ 的前缀积和后缀积，就能直接得到分子的值了。&lt;/p&gt;
&lt;p&gt;对于分母，假设 $i$ 已经固定，那么有：
$$
\begin{aligned}
&amp;#x26;\dfrac{1}{\prod\limits_{i\not ={j}}(i-j)}\ =
&amp;#x26;\dfrac{1}{i(i-1)(i-2) \dots  1 \times(-1) \dots  (k+2-i-1)(k+2-i)}\=&amp;#x26;(-1)^{k+2-i}\dfrac{1}{i!  (k+2-i)!}
\end{aligned}
$$
所以我们预处理阶乘，再求逆元，就可以快速得到分母的值。&lt;/p&gt;
&lt;p&gt;最后，把变换后的式子代入 $S_k(n)$ 的表达式：
$$
S_k(n)=\sum_{i=1}^{k+2}S_k(i)(-1)^{k+2-i}\dfrac{(\prod\limits_{j=1}^{i-1}(n-j))(\prod\limits_{j=i+1}^{k+2}(n-j))}{i!(k+2-i)!}
$$
时间复杂度是 $\mathcal{O}(k\log k)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXK = 1e6 + 10, mod = 1e9 + 7;
int n, k;
int pre[MAXK], suf[MAXK], fac[MAXK];
int power(int x, int k) {
	int ret = 1;
	while(k) {
		if(k &amp;#x26; 1) ret = ret * x % mod;
		k &gt;&gt;= 1;
		x = x * x % mod;
	}
	return ret;
}
inline int inv(int x) {
	return power(x, mod - 2);
}
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;k);
	fac[0] = pre[0] = 1;
	for(int i = 1; i &amp;#x3C;= k + 2; i++) {
		pre[i] = pre[i - 1] * (n - i) % mod;
		fac[i] = fac[i - 1] * i % mod;
	}
	suf[k + 3] = 1;
	for(int i = k + 2; i &gt;= 1; i--) suf[i] = suf[i + 1] * (n - i) % mod;
	int tmp = 0, ans = 0;
	for(int i = 1; i &amp;#x3C;= k + 2; i++) {
		tmp = (tmp + power(i, k)) % mod;
		int a = pre[i - 1] * suf[i + 1] % mod;
		int b = fac[i - 1] * fac[k + 2 - i] % mod;
		if((k - i) % 2 == 0) ans = (ans + tmp * a % mod * inv(b) % mod) % mod;
		else ans = (ans - tmp * a % mod * inv(b) % mod) % mod;
	}
	cout &amp;#x3C;&amp;#x3C; (ans + mod) % mod &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;进一步优化&lt;/h2&gt;
&lt;p&gt;上面做法有两个部分的复杂度带 $\log k$，阶乘的逆元可以线性预处理，问题就只剩：如何快速预处理
$$
1^k, 2^k, 3^k, \cdots, k^k
$$
发现 $f(t)=t^k$ 是完全积性函数，于是可以只求所有质数的 $k$ 次幂，然后线性筛。&lt;/p&gt;
&lt;p&gt;这样整体复杂度大约是 $\mathcal{O}(k)$ 的。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/blog/luckyblock/solution-cf622f&quot;&gt;地靈殿 - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://henryhuang.blog.luogu.org/solution-cf622f&quot;&gt;HenryHuang 的博客 - 洛谷博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://oi-wiki.com/math/poly/lagrange/&quot;&gt;拉格朗日插值 - OI Wiki&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>【UVA-11475】Extend to Palindrome</title><link>https://www.tonyyin0418.com/blog/oi-solution/uva11475</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/uva11475</guid><description>算法题解</description><pubDate>Fri, 27 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;对于一个字符串 $s$，求出一个尽可能短的回文字符串 $s^{&lt;em&gt;}$，同时使得 $s$ 是 $s^{&lt;/em&gt;}$ 的前缀。&lt;/p&gt;
&lt;p&gt;$|s|\leq 10^5$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;前置知识：&lt;a href=&quot;http://oi-wiki.com/string/manacher/&quot;&gt;Manacher - OI Wiki&lt;/a&gt;，，题解同步发表于 &lt;a href=&quot;https://www.tonyyin.top/2021/08/uva-11475/&quot;&gt;TonyYin&apos;s Blog&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;首先，先考虑如何满足 &lt;strong&gt;$s$ 是 $s^{&lt;em&gt;}$ 的前缀，且 $s^{&lt;/em&gt;}$ 是回文串这两个条件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;经过简单分析可以得到，$s^{*}$ 回文中心左侧一定都与 $s$ 的前缀相同。&lt;/p&gt;
&lt;p&gt;因此只需要&lt;strong&gt;找到最小的 $i$，其回文半径可以延续到串末&lt;/strong&gt;即可。&lt;/p&gt;
&lt;p&gt;形式化地，要找到最小的 $i$ 满足 $s[2\times i-len]\sim s[i]\sim s[len]$ 为回文串。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;通过 $\rm{Manacher}$ 算法解决问题。&lt;/p&gt;
&lt;p&gt;在进行 $\rm{Manacher}$ 的过程中，只要当前点 $i$ 的回文半径可以延续到串末，$i$ 即为 $s^{*}$ 的回文中心。&lt;/p&gt;
&lt;p&gt;找到中心之后，把 $s^{*}$ 还原出来即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXLEN = 1e5 + 10;
char in[MAXLEN], s[MAXLEN &amp;#x3C;&amp;#x3C; 1];
int len;
void init() {
	int lenin = strlen(in); len = 0;
	s[++len] = &apos;*&apos;; s[++len] = &apos;|&apos;;
	for(int i = 0; 
        i &amp;#x3C; strlen(in); i++) {
		s[++len] = in[i]; s[++len] = &apos;|&apos;;
	}
}
int p[MAXLEN &amp;#x3C;&amp;#x3C; 1];//以i为中心的最大回文半径
int main() {
	while(scanf(&quot;%s&quot;, in) != EOF) {
		int ans = 0;
		int mx = 0, id = 0; init();
		for(int i = 1; i &amp;#x3C;= len; i++) {
			if(i &amp;#x3C; mx) p[i] = min(mx - i, p[2 * id - i]);
			else p[i] = 1;
			while(s[i + p[i]] == s[i - p[i]]) p[i]++;
			if(i + p[i] &gt;= mx) {
				id = i; mx = i + p[i];
			}
			if(mx &gt;= len) {
				ans = i;
				break;
			}
		}
		for(int i = 2; i &amp;#x3C;= ans; i++) if(s[i] != &apos;|&apos;) putchar(s[i]);
		for(int i = ans - 1; i &gt;= 2; i--) if(s[i] != &apos;|&apos;) putchar(s[i]);
		putchar(&apos;\n&apos;);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3317】[SDOI2014]重建</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3317</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3317</guid><description>算法题解</description><pubDate>Wed, 25 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;给定无向图，每条边以 $p_i$ 的概率存在。求存在的边构成一棵生成树的概率。&lt;/p&gt;
&lt;p&gt;点数 $2\leq N\leq 50$，无重边。&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;p&gt;前置知识：&lt;a href=&quot;http://oi-wiki.com/graph/matrix-tree/&quot;&gt;矩阵树定理 - OI Wiki&lt;/a&gt;，&lt;a href=&quot;http://oi-wiki.com/math/gauss/#_12&quot;&gt;行列式计算 - OI Wiki&lt;/a&gt;，题解同步发表于 &lt;a href=&quot;https://www.tonyyin.top/2021/08/luogu-p3317/&quot;&gt;TonyYin&apos;s Blog&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;看到 $N\leq 50$ 的范围和生成树，容易想到&lt;strong&gt;矩阵树定理&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;矩阵树定理&lt;/h3&gt;
&lt;p&gt;对于一个无向图 $G$，设边 $e_i=(u_i, v_i)$，其边权为 $v_i = w(u_i, v_i)$.&lt;/p&gt;
&lt;p&gt;定义邻接矩阵 $A$，$A_{i, j}=w(i, j)$.&lt;/p&gt;
&lt;p&gt;定义度数矩阵 $D$，$D_{i, i}=\operatorname{deg}_{i}$，存储每个点的度数。&lt;/p&gt;
&lt;p&gt;定义基尔霍夫矩阵 $L$ ，$L=D-A$.&lt;/p&gt;
&lt;p&gt;任取 $i\in[1, N]$，将基尔霍夫矩阵删去第 $i$ 行和第 $i$ 列，构成一个 $(N-1)\times (N-1)$ 的子矩阵，不妨记为 $L&apos;$.&lt;/p&gt;
&lt;p&gt;根据无向图的矩阵树定理，有：
$$
\det L&apos; = \sum_{T}{\prod_{e\in T}v_e}
$$
对矩阵 $L&apos;$ 计算行列式，得到的&lt;strong&gt;结果即为：每棵生成树中，所有边权乘积的和&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;由概率的性质，我们最终求的答案可以表示为：
$$
\operatorname{Ans}=\sum_T[{\prod_{e\in T}p_e \prod_{e\notin T}(1-p_e)}]
$$
这里一定不要忘记乘上：$\prod_{e\notin T}(1-p_e)$.&lt;/p&gt;
&lt;p&gt;注意到 $\operatorname{Ans}$ 的表示式与矩阵树定理很像，考虑如何将其转化成矩阵树定理的标准形式。&lt;/p&gt;
&lt;p&gt;要转化为标准形式，我们需要去掉 $\prod_{e\notin T}$ 这个部分，具体转化步骤：
$$
\begin{align}
\operatorname{Ans}&amp;#x26;=
\sum_T[{\prod_{e\in T}p_e \prod_{e\notin T}(1-p_e)}]\
&amp;#x26;=\sum_T[{\prod_{e\in T}p_e \frac{\prod_{e}{(1-p_e)}}{\prod_{e\in T}(1-p_e)}}]\
\end{align}
$$
注意到分子部分为常数，于是：
$$
\begin{align}
\operatorname{Ans}&amp;#x26;=
\sum_T[{\prod_{e\in T}p_e \frac{\prod_{e}{(1-p_e)}}{\prod_{e\in T}(1-p_e)}}]\
&amp;#x26;=\prod_{e}{(1-p_e)}\sum_T{\prod_{e\in T}\frac{p_e}{1-p_e}}\
\end{align}
$$
这样，只需要将边权定为 $\dfrac{p_e}{1-p_e}$，之后利用矩阵树定理求解即可。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;p&gt;实现时，为了防止边权的分母为 $0$，若 $p_e=1$，将 $p_e\leftarrow p_e+\varepsilon$ 即可避免问题发生。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 100;
const double eps = 1e-10;
int n;
double g[MAXN][MAXN], A[MAXN][MAXN];
double det() {
	for(int col = 2; col &amp;#x3C;= n; col++) {
		int max_row = col;
		for(int i = col + 1; i &amp;#x3C;= n; i++)
			if(A[i][col] &gt; A[max_row][col]) max_row = i;
		if(max_row != col) swap(A[col], A[max_row]);
		for(int row = col + 1; row &amp;#x3C;= n; row++) {
			double t = A[row][col] / A[col][col];
			for(int i = 2; i &amp;#x3C;= n; i++) {
				A[row][i] -= A[col][i] * t;
			}
		}
	}
	double ans = 1.0;
	for(int i = 2; i &amp;#x3C;= n; i++) ans *= A[i][i];
	return ans;
}
int main() {
	scanf(&quot;%d&quot;, &amp;#x26;n);
	double prod = 1.0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = 1; j &amp;#x3C;= n; j++) {
			scanf(&quot;%lf&quot;, &amp;#x26;g[i][j]); g[i][j] -= eps;
			A[i][j] -= g[i][j] / (1.0 - g[i][j]);
		}
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = i + 1; j &amp;#x3C;= n; j++) {
			A[i][i] += g[i][j] / (1.0 - g[i][j]);
			A[j][j] += g[i][j] / (1.0 - g[i][j]);
			prod *= (1.0 - g[i][j]);
		}
	}
	cout &amp;#x3C;&amp;#x3C; prod * det() &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P7835】「Wdoi-3」夜雀 dreaming</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7835</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7835</guid><description>算法题解</description><pubDate>Tue, 24 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;考场解法，比其他题解复杂，但好像顺便说明了其他题解的结论？&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/paste/2dwm3f7z&quot;&gt;剪贴板&lt;/a&gt; 里的题目描述比较友好。&lt;/p&gt;
&lt;p&gt;有无限个盒子，从 $1$ 开始编号，有 $n$ 个球，从 $0$ 开始编号。有 $k$ 个人。&lt;/p&gt;
&lt;p&gt;$k$  个人往盒子里放球，第 $i$ 个人只能在编号为 $t_i$ 的倍数的盒子放球，第 $j$ 次放球的颜色编号为 $[x_i+(j-1)\times y_i]\bmod n$.&lt;/p&gt;
&lt;p&gt;求：编号最小的盒子，满足盒子里存在两球颜色不同。&lt;/p&gt;
&lt;p&gt;$n\leq 10^9$，$k\leq 10^3$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;注意到 $k\leq 10^3$，容易想到暴力枚举两个人，设这两人为 $(u, v)$.&lt;/p&gt;
&lt;p&gt;$u$ 和 $v$ 会在一些盒子里同时放球，这些盒子的编号为 $c\times \operatorname{lcm}(t_u, t_v)$，$c$ 是正整数。&lt;/p&gt;
&lt;p&gt;对于第 $c\times \operatorname{lcm}(t_u, t_v)$ 个盒子，是 $u$ 第 $\dfrac{\operatorname{lcm}(t_u, t_v)}{t_u}\times c$  次放球，是 $v$ 第 $\dfrac{\operatorname{lcm}(t_u, t_v)}{t_v}\times c$ 次放球。&lt;/p&gt;
&lt;p&gt;对于某个 $c$，假设第 $c\times \operatorname{lcm}(t_u, t_v)$ 个盒子中，$u$ 和 $v$ 所放球的颜色相同，那么有同余方程：
$$
x_u+(c\times \frac{\operatorname{lcm}(t_u, t_v)}{t_u}-1)\cdot y_u
\equiv
x_v+(c\times \frac{\operatorname{lcm}(t_u, t_v)}{t_v}-1)\cdot y_v
\pmod{n}
$$
其中只有 $c$ 是未知量，整理一下式子，即可用 $\rm{ExGcd}$ 求出 $c$ 的最小正整数解 $c_{min}$.&lt;/p&gt;
&lt;p&gt;若 $c_{min}\geq 2$，说明第 $\operatorname{lcm}(t_u, t_v)$ 个盒子满足条件，用 $\operatorname{lcm}(t_u, t_v)$ 更新答案。&lt;/p&gt;
&lt;p&gt;若 $c_{min}=1$，继续判断 $c=2$ 是否为可行解。若 $c\neq 2$，则用 $2\times \operatorname{lcm}(t_u, t_v)$ 更新答案。否则  $(u, v)$ 这种情况无解，对答案无贡献。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 1008;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, k;
inline int exgcd(int a, int b, int &amp;#x26;x, int &amp;#x26;y) {
	if(b == 0) {
		x = 1, y = 0;
		return a;
	}
	int r = exgcd(b, a % b, x, y);
	int t = x;
	x = y;
	y = t - a / b * y;
	return r;
}
inline int gcd(int a, int b) {
	if(b == 0) return a;
	return gcd(b, a % b);
}
int t[MAXN], x[MAXN], y[MAXN];
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;k);
	for(int i = 1; i &amp;#x3C;= k; i++) {
		scanf(&quot;%lld%lld%lld&quot;, &amp;#x26;t[i], &amp;#x26;x[i], &amp;#x26;y[i]);
	}
	int ans = inf;
	for(int i = 1; i &amp;#x3C;= k; i++) {
		for(int j = 1; j &amp;#x3C;= k; j++) {
			if(i == j) continue;
			int xx, yy;
			int Lcm = t[i] / gcd(t[i], t[j]) * t[j];
			int a = Lcm / t[i] * y[i] - Lcm / t[j] * y[j];
			int b = x[j] - x[i] + y[i] - y[j];
			if(a &amp;#x3C; 0 &amp;#x26;&amp;#x26; b &amp;#x3C; 0) continue;
			int d = gcd(a, n);
			if(b % d != 0) {
				ans = min(ans, Lcm);
				continue;
			} else {
				exgcd(a, n, xx, yy);
				int tmp = ((b/d*xx) % (n/d) + (n/d)) % (n/d);
				if(tmp &gt;= 2) ans = min(ans, Lcm);
				if(tmp == 1 &amp;#x26;&amp;#x26; (n/d) != 1) {
					ans = min(ans, 2 * Lcm);
				}
			}
		}
	}
	if(ans == inf) cout &amp;#x3C;&amp;#x3C; &quot;Mystia will cook forever...\n&quot;;
	else cout &amp;#x3C;&amp;#x3C; ans - 1 &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P7842】「PMOI-4」可怜的团主</title><link>https://www.tonyyin0418.com/blog/oi-solution/p7842</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p7842</guid><description>算法题解</description><pubDate>Tue, 24 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个 $n$ 个点，$m$ 条边的简单连通无向图，问：是否能实现以下两个操作中的一个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到&lt;strong&gt;恰好 $\lceil\frac{n}{6}\rceil$​​​ 条不同的&lt;/strong&gt;路径，​使每个点都至少被一条路径经过。（&lt;strong&gt;每条路径上&lt;/strong&gt;，每个点至多经过一次）&lt;/li&gt;
&lt;li&gt;找到&lt;strong&gt;恰好 $\lfloor\frac{n}{3}\rfloor$​​​​ 个&lt;/strong&gt;点，使其中&lt;strong&gt;两两没有边&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若能实现操作1，输出每条路径；若能实现操作2，输出满足条件的这些点。&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$3\leq n\leq 10^3$，$3\leq m\leq \dfrac{n(n-1)}{2}$.&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$​ $1$​&lt;/h2&gt;
&lt;p&gt;对于 $20%$ 的数据，$n, m\leq 10$.&lt;/p&gt;
&lt;p&gt;暴力搜索即可。期望得分 20 分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int sta = 0; sta &amp;#x3C; (1 &amp;#x3C;&amp;#x3C; n); sta++) {
    int cnt = 0; bool wrong = false;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        if(sta &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (i - 1))) cnt++;
    }
    if(cnt != n / 3) continue;
    for(int i = 1; i &amp;#x3C;= n; i++) if(sta &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (i - 1))) {
        for(int j = 1; j &amp;#x3C;= n; j++) if(sta &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (j - 1))) {
            if(i == j) continue;
            if(mapp[i][j]) {wrong = true; break;}
        }
        if(wrong) break;
    }
    if(!wrong) {
        cout &amp;#x3C;&amp;#x3C; 2 &amp;#x3C;&amp;#x3C; endl;
        for(int i = 1; i &amp;#x3C;= n; i++) {
            if(sta &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (i - 1))) cout &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot; &quot;;
        } cout &amp;#x3C;&amp;#x3C; endl;
        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;$\rm{Subtask}$ $2$&lt;/h2&gt;
&lt;p&gt;对于另外 $20%$​ 的数据，图是一棵树。&lt;/p&gt;
&lt;h4&gt;算法一&lt;/h4&gt;
&lt;p&gt;将树上的点按深度奇偶性分类，然后选择这两类中较大的那一个作为独立集。&lt;/p&gt;
&lt;p&gt;显然这个独立集大小大于 $n/2$，满足要求。&lt;/p&gt;
&lt;p&gt;很好写，就不给代码了。&lt;/p&gt;
&lt;h4&gt;算法二&lt;/h4&gt;
&lt;p&gt;考虑满足操作二的构造方法，这更接近正解。&lt;/p&gt;
&lt;p&gt;任意找一个点为根，找出所有 $x$ 个叶子。&lt;/p&gt;
&lt;p&gt;如果 $x\leq \lfloor\frac{n}{3}\rfloor$​，那么满足操作一。&lt;/p&gt;
&lt;p&gt;否则，构造出一些以这些叶子为两端的路径，使得这些路径覆盖到图上所有的点。&lt;/p&gt;
&lt;p&gt;可以证明，这样的路径一定存在，并且最小覆盖数是 $\lceil\frac{x}{2}\rceil$. 下面给出可行的构造方法，即可证明。&lt;/p&gt;
&lt;h4&gt;构造方案&lt;/h4&gt;
&lt;p&gt;首先，将叶子任意两两配对。若 $x$ 为奇数，那么加一个点编号为 $n+1$，直接连在根节点 $1$ 下面。&lt;/p&gt;
&lt;p&gt;显然配对不一定能覆盖所有的点，所以考虑进行若干次调整。&lt;/p&gt;
&lt;p&gt;对于每次调整，我们&lt;strong&gt;找到一个没有被覆盖的点 $p$，之后找 $p$ 的任意两个子树，从这两个子树中分别找到任意两个叶子&lt;/strong&gt;，记为 $(u, v)$，$(u&apos;, v&apos;)$.&lt;/p&gt;
&lt;p&gt;直接把这两条路径变换一下配对顺序，变为 $(u, v&apos;)$ 和 $(v, u&apos;)$​. 这样一定可以使 $p$ 被覆盖到，至少多覆盖了一个点。&lt;/p&gt;
&lt;p&gt;由于每次操作，都会多覆盖 $\geq 1$ 个点，所以最多进行 $n$ 次，时间复杂度为 $\mathcal{O}(n^2)$，可以接受。&lt;/p&gt;
&lt;p&gt;可以证明操作四个点一定能找到，操作一定可行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++) if(!covered[i]) {
    int son_cnt = 0, leaf1, leaf2, leaf3, leaf4;
    for(int j = head[i]; j; j = edge[j].nxt) {
        int v = edge[j].to;
        son_cnt++;
        if(son_cnt == 1) {
            leaf1 = Get_leaf(v);
            leaf2 = Pair[leaf1];
        } else if(son_cnt == 2) {
            leaf3 = Get_leaf(v);
            leaf4 = Pair[leaf3];
            break;
        }
    }
    Make_pair(leaf1, leaf3); Make_pair(leaf2, leaf4);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;操作一定可行的证明&lt;/h4&gt;
&lt;p&gt;对于一个没有被覆盖的点 $p$，其每个子树内叶子个数一定 $\geq 2$，下面给出证明。&lt;/p&gt;
&lt;p&gt;由于没有被覆盖，所以子树内的每条路径，都仅在儿子的子树内，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/08/24/7b095d67e8e61.jpeg&quot; alt=&quot;例 - 图.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\rm{y}$ 存在一棵子树仅有一个叶子。红色节点相关的路径，必定会覆盖到 $\rm{y}$.&lt;/p&gt;
&lt;p&gt;$\rm{x}$ 是一种不被覆盖的可行情况。&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $3$​&lt;/h2&gt;
&lt;p&gt;容易想到，在无向图中找一棵生成树，尝试继续使用上面的方法进行构造。&lt;/p&gt;
&lt;p&gt;注意到，$\rm{Subtask}$ $2$ 中，利用了树的重要性质：叶子之间没有边。&lt;/p&gt;
&lt;p&gt;因此，我们在原图上找到DFS树。这样能满足叶子节点之间，在原图上没有边。&lt;/p&gt;
&lt;p&gt;使用 $\rm{Subtask}$ $2$ 的算法二解决即可。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;p&gt;代码没有给出头文件和 &lt;code&gt;read()&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dfs1(int)&lt;/code&gt;，&lt;code&gt;dfs2(int, int)&lt;/code&gt;，&lt;code&gt;get_lca(int, int)&lt;/code&gt; 实现了树剖LCA。&lt;/p&gt;
&lt;p&gt;由于本题时间复杂度为 $\mathcal{O}(n^2)$，可以不用这种方法求 LCA，暴力找 LCA 也可以通过。&lt;/p&gt;
&lt;p&gt;代码细节较多）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;using namespace std;
const int MAXN = 1e3 + 10, MAXM = 2e6 + 10;

int n, m, extra;
vector&amp;#x3C;int&gt; mp[MAXN];
struct Edge{
	int from, to, nxt;
} edge[MAXM];
int head[MAXN], e_cnt = 0;
void add_edge(int u, int v) {
	edge[++e_cnt] = (Edge){u, v, head[u]};
	head[u] = e_cnt;
}
int vis[MAXN], dep[MAXN], fa[MAXN], deg[MAXN];
void Get_DFS_Tree(int u, int father) {
	vis[u] = true;
	dep[u] = dep[father] + 1; fa[u] = father;
	for(int i = 0; i &amp;#x3C; mp[u].size(); i++) {
		int v = mp[u][i]; if(vis[v]) continue;
		Get_DFS_Tree(v, u);
		deg[u]++; deg[v]++;
		add_edge(u, v); add_edge(v, u);
	}
}
int siz[MAXN], son[MAXN], dfn[MAXN], top[MAXN], tot;
void dfs1(int u) {
	siz[u] = 1;
	for(int i = head[u]; i; i = edge[i].nxt) {
		int v = edge[i].to; if(v == fa[u]) continue;
		dfs1(v);
		if(!son[u] || siz[v] &gt; siz[son[u]]) son[u] = v;
		siz[u] += siz[v];
	}
}
void dfs2(int u, int topf) {
	top[u] = topf; dfn[u] = ++tot;
	if(!son[u]) return;
	dfs2(son[u], topf);
	for(int i = head[u]; i; i = edge[i].nxt) {
		int v = edge[i].to; if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
}
int get_lca(int u, int v) {
	while(top[u] != top[v]) {
		if(dep[top[u]] &amp;#x3C; dep[top[v]]) swap(u, v);
		u = fa[top[u]];
	}
	if(dep[u] &amp;#x3C; dep[v]) swap(u, v);
	return v;
}
int leaf[MAXN], leaf_cnt;
int Pair[MAXN], covered[MAXN];
void Cover(int x, int y) {
	if(x == y) {covered[x] = 1; return;}
	do{
		covered[x] = covered[y] = 1;
		if(dep[x] &gt; dep[y]) x = fa[x];
		else y = fa[y];
	} while(x != y);
}
void Make_pair(int x, int y) {
	Pair[x] = y; Pair[y] = x;
	Cover(x, y);
}
int Get_leaf(int u) {
	while(deg[u] != 1) {
		for(int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].to; if(v != fa[u]) {u = v; break;}
		}
	}
	return u;
}
void print_path(int u, int v) {
	if(u == extra) u = fa[u];
	if(v == extra) v = fa[v];
	int lca = get_lca(u, v);
	vector&amp;#x3C;int&gt; p1, p2;
	int x = u; while(x != lca) {p1.push_back(x); x = fa[x];}
	x = v; while(x != lca) {p2.push_back(x); x = fa[x];}
	printf(&quot;%d &quot;, p1.size() + p2.size() + 1);
	for(int i = 0; i &amp;#x3C; p1.size(); i++) printf(&quot;%d &quot;, p1[i]);
	printf(&quot;%d &quot;, lca);
	for(int i = p2.size() - 1; i &gt;= 0; i--) printf(&quot;%d &quot;, p2[i]);
	putchar(&apos;\n&apos;);
}
int main() {
	srand(time(0));
	n = read(); m = read();
	int rest = (n + 5) / 6;
	for(int i = 1; i &amp;#x3C;= m; i++) {
		int u = read(), v = read();
		mp[u].push_back(v); mp[v].push_back(u);
	}
	Get_DFS_Tree(1, 0);
	dfs1(1); dfs2(1, 1);
	for(int i = 2; i &amp;#x3C;= n; i++) if(deg[i] == 1) leaf[++leaf_cnt] = i;
	if(leaf_cnt &gt;= n / 3) {
		putchar(&apos;2&apos;); putchar(&apos;\n&apos;);
		for(int i = 1; i &amp;#x3C;= n / 3; i++) printf(&quot;%d &quot;, leaf[i]);
		return 0;
	}
	if(deg[1] == 1) leaf[++leaf_cnt] = 1;
	if(leaf_cnt &amp;#x26; 1) {
		n++; fa[n] = 1; deg[n] = 1; leaf[++leaf_cnt] = n;
		extra = n;
		add_edge(1, n); add_edge(n, 1);
	}
	for(int i = 2; i &amp;#x3C;= leaf_cnt - 1; i += 2) Make_pair(leaf[i], leaf[i + 1]);
	Make_pair(leaf[1], leaf[leaf_cnt]);
	for(int i = 1; i &amp;#x3C;= n; i++) if(!covered[i]) {
		int son_cnt = 0, leaf1, leaf2, leaf3, leaf4;
		for(int j = head[i]; j; j = edge[j].nxt) {
			int v = edge[j].to;
			son_cnt++;
			if(son_cnt == 1) {
				leaf1 = Get_leaf(v);
				leaf2 = Pair[leaf1];
			} else if(son_cnt == 2) {
				leaf3 = Get_leaf(v);
				leaf4 = Pair[leaf3];
				break;
			}
		}
		Make_pair(leaf1, leaf3); Make_pair(leaf2, leaf4);
	}
	putchar(&apos;1&apos;); putchar(&apos;\n&apos;);
	for(int i = 1; i &amp;#x3C;= leaf_cnt; i++) {
		int u = leaf[i], v = Pair[leaf[i]];
		if(u &gt; v) continue; //保证每条路径只被输出一次
		print_path(u, v); rest--;
	}
	for(int i = 1; i &amp;#x3C;= rest; i++) {
		print_path(rand() % n + 1, i);//这个我也不清楚咋解决
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【2021北京省选模拟】奇怪的数列</title><link>https://www.tonyyin0418.com/blog/oi-solution/2021bjsx-2c</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/2021bjsx-2c</guid><description>算法题解</description><pubDate>Mon, 23 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;有一个长度为 $n$ 的数字序列 $a$，对这个序列的任意一个连续子串，求所有数字之和，重复出现的数字只被统计一次。问第 $k$ 大的和是多少。&lt;/p&gt;
&lt;p&gt;$n\leq 10^5$，$k\leq 2\times 10^5$，$0\leq |a_i|\leq 10^9$.&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;p&gt;以右端点为横坐标，第 $i$ 棵线段树维护：以 $i$ 作为左端点的所有&lt;strong&gt;区间和的最大值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先考虑第 $1$ 棵线段树。由于 $a_i$ 较大，先借助 $\rm{map}$ 处理出去重后的前缀和。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++) {
    pre_sum[i] = pre_sum[i - 1];
    if(!mp[in[i]]) {
        pre_sum[i] += in[i];
        mp[in[i]] = 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就知道了：所有以 $1$ 作为左端点的区间和。依此直接建树即可得到第 $1$ 棵线段树。&lt;/p&gt;
&lt;p&gt;对于后面的 $n-1$ 棵线段树，如果采取同样的方法，时间和空间都会炸。&lt;/p&gt;
&lt;p&gt;我们发现，第 $i$ 棵线段树和第 $i-1$ 棵线段树差距不大，因此使用可持久化线段树，使线段树之间共享节点。&lt;/p&gt;
&lt;p&gt;进一步，从第 $i$ 棵线段树转移到第 $i+1$ 棵线段树，只变了 $i$  到下一次 $a_i$ 出现的位置之前这段区间。右端点在这段区间内的线段都减少了$a_i$.&lt;/p&gt;
&lt;p&gt;同样，我们借助 $\rm{map}$ 预处理出每个元素 $a_i$ 下一次出现的位置 $\operatorname{next}_{a_i}$，每次就可以通过上一棵线段树直接转移出当前所求。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void add(int &amp;#x26;id, int his, int l, int r, int L, int R, int v) {}

for(int l = 2; l &amp;#x3C;= n; l++) { //l为左端点
    root[l] = root[l - 1];
    add(root[l], root[l - 1], 1, n, l - 1, nxt[l - 1] - 1, -in[l - 1]);
    add(root[l], root[l], 1, n, l - 1, l - 1, -inf);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用主席树维护之后，开始解决第 $k$ 大的问题。&lt;/p&gt;
&lt;p&gt;由于 $k$ 不大，$\mathcal{O}(k\log)$ 的复杂度可以接受，直接用大根堆维护。&lt;/p&gt;
&lt;p&gt;先找出以每个 $i$ 作为左端点的最大区间和，共 $n$ 个，加入大根堆。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;priority_queue&amp;#x3C;pair&amp;#x3C;pair&amp;#x3C;int, int&gt;, int&gt; &gt; q;
for(int i = 1; i &amp;#x3C;= n; i++) q.push(make_pair(t[root[i]].mx, i));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后每次取出后，利用线段树找到另一个最大的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int ans;
while(k--) {
    pair&amp;#x3C;pair&amp;#x3C;int, int&gt;, int&gt; now = q.top(); q.pop();
    ans = now.first.first;
    add(root[now.second], root[now.second], 1, n, now.first.second, now.first.second, -inf);
    q.push(make_pair(t[root[now.second]].mx, now.second));
}
cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;

inline int read() {
	int ret = 0, ch = getchar(), f = 1;
	while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) {
		if(ch == &apos;-&apos;) f = -1;
		ch = getchar();
	}
	while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) {
		ret = ret * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	return ret * f;
}
const int MAXN = 1e5 + 10;
const int inf = 0x3f3f3f3f3f3f3f3fll;
int n, k;
int in[MAXN], pre_sum[MAXN];
int nxt[MAXN];
map&amp;#x3C;int, int&gt; mp;
struct node{
	pair&amp;#x3C;int, int&gt; mx;
	int add_mark, l, r;
} t[MAXN &amp;#x3C;&amp;#x3C; 8];
int point_cnt = 0;
void push_up(int id) {
	t[id].mx = max(t[t[id].l].mx, t[t[id].r].mx);
}
void build(int &amp;#x26;id, int l, int r) {
	id = ++point_cnt;
	if(l == r) {
		t[id].mx = make_pair(pre_sum[l], l);
		return;
	}
	int mid = (l + r) &gt;&gt; 1;
	build(t[id].l, l, mid);
	build(t[id].r, mid + 1, r);
	push_up(id);
}
int root[MAXN];
void add_point(int &amp;#x26;id, int v) {
	if(!id) return;
	t[++point_cnt] = t[id]; id = point_cnt;
	if(v == -inf) {
		t[id].mx.first = t[id].add_mark = v;
	} else {
		t[id].mx.first += v; t[id].add_mark += v;
	}
}
void push_down(int id) {
	if(!t[id].add_mark) return;
	if(t[id].l) add_point(t[id].l, t[id].add_mark);
	if(t[id].r) add_point(t[id].r, t[id].add_mark);
	t[id].add_mark = 0;
}
void add(int &amp;#x26;id, int his, int l, int r, int L, int R, int v) {
	id = ++point_cnt;
	t[id].mx.first = -inf;
	if(l == L &amp;#x26;&amp;#x26; r == R) {
		t[id] = t[his];
		add_point(id, v);
		return;
	}
	push_down(his);
	int mid = (l + r) &gt;&gt; 1;
	if(R &amp;#x3C;= mid) {
		t[id].r = t[his].r;
		add(t[id].l, t[his].l, l, mid, L, R, v);
	} else if(L &gt; mid) {
		t[id].l = t[his].l;
		add(t[id].r, t[his].r, mid + 1, r, L, R, v);
	} else {
		add(t[id].l, t[his].l, l, mid, L, mid, v);
		add(t[id].r, t[his].r, mid + 1, r, mid + 1, R, v);
	}
	push_up(id);
}
signed main() {
	n = read(); k = read();
	for(int i = 1; i &amp;#x3C;= n; i++) in[i] = read();
	for(int i = 1; i &amp;#x3C;= n; i++) nxt[i] = n + 1;
	for(int i = n; i &gt;= 1; i--) {
		if(mp[in[i]]) nxt[i] = mp[in[i]];
		mp[in[i]] = i;
	}
	mp.clear();
	for(int i = 1; i &amp;#x3C;= n; i++) {
		pre_sum[i] = pre_sum[i - 1];
		if(!mp[in[i]]) {
			pre_sum[i] += in[i];
			mp[in[i]] = 1;
		}
	}
	t[0].mx = make_pair(-inf-1, 0);
	build(root[1], 1, n);
	for(int l = 2; l &amp;#x3C;= n; l++) {
		root[l] = root[l - 1];
		add(root[l], root[l - 1], 1, n, l - 1, nxt[l - 1] - 1, -in[l - 1]);
 		add(root[l], root[l], 1, n, l - 1, l - 1, -inf);
	}
	priority_queue&amp;#x3C;pair&amp;#x3C;pair&amp;#x3C;int, int&gt;, int&gt; &gt; q;
	for(int i = 1; i &amp;#x3C;= n; i++) q.push(make_pair(t[root[i]].mx, i));
	int ans;
	while(k--) {
		pair&amp;#x3C;pair&amp;#x3C;int, int&gt;, int&gt; now = q.top(); q.pop();
		ans = now.first.first;
		add(root[now.second], root[now.second], 1, n, now.first.second, now.first.second, -inf);
		q.push(make_pair(t[root[now.second]].mx, now.second));
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P2598】[ZJOI2009]狼和羊的故事</title><link>https://www.tonyyin0418.com/blog/oi-solution/p2598</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p2598</guid><description>算法题解</description><pubDate>Wed, 14 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个 $n\times m$ 的矩阵，矩阵上每一个点可能是狼、羊或者空地。&lt;/p&gt;
&lt;p&gt;你需要在格子的边界修建篱笆，使得任意两个狼与羊的格子不连通。&lt;/p&gt;
&lt;p&gt;给一个格子的一条边界修建篱笆，那么篱笆总长度增加 $1$，求篱笆的最短长度。&lt;/p&gt;
&lt;p&gt;$n, m\leq 100$.&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;p&gt;这道题，需要把狼和羊分开，所以想到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;源点向所有狼连容量为 $\inf$ 的边；&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所有羊向汇点连容量为 $\inf$ 的边；&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样使狼和羊一定分别属于两个集合。&lt;/p&gt;
&lt;p&gt;之后对矩阵上的限制进行模拟，&lt;strong&gt;所有点向四周连容量为 $1$ 的边。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样断掉一条边，就等价于在矩阵中选一条边界拦住。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;
const int MAXN = 10008, MAXM = 1e7;
const int inf = 0x3f3f3f3f;
int n, m, S, T;
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int num[MAXN][MAXN];
int get_id(int x, int y) {
	return (x - 1) * m + y;
}
struct Edge{
	int to, val, nxt;
} edge[MAXM];
int head[MAXN], e_cnt = 1;
void add(int u, int v, int w) {
	edge[++e_cnt] = (Edge){v, w, head[u]};
	head[u] = e_cnt;
}
void add_edge(int u, int v, int w) {
	add(u, v, w); add(v, u, 0);
}
int lev[MAXN];
bool bfs() {
	memset(lev, -1, sizeof(lev)); lev[S] = 0;
	queue&amp;#x3C;int&gt; q; q.push(S);
	while(!q.empty()) {
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].to, val = edge[i].val;
			if(lev[v] == -1 &amp;#x26;&amp;#x26; val) {
				lev[v] = lev[u] + 1;
				q.push(v);
			}
		}
	}
	return (lev[T] != -1);
}
int dfs(int u, int flow) {
	if(u == T) return flow;
	int ret = 0;
	for(int i = head[u]; i; i = edge[i].nxt) {
		int v = edge[i].to, val = edge[i].val;
		if(lev[v] == lev[u] + 1 &amp;#x26;&amp;#x26; val) {
			int tmp = dfs(v, min(flow, val));
			edge[i].val -= tmp;
			edge[i ^ 1].val += tmp;
			flow -= tmp;
			ret += tmp;
			if(flow == 0) break;
		}
	}
	if(ret == 0) lev[u] = -1;
	return ret;
}
int Dinic() {
	int ret = 0;
	while(bfs()) {
		ret += dfs(S, inf);
	}
	return ret;
}
int main() {
	scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;m);
	S = 0; T = n * m + 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = 1; j &amp;#x3C;= m; j++) {
			scanf(&quot;%d&quot;, &amp;#x26;num[i][j]);
			int id = get_id(i, j);
			if(num[i][j] == 1) add_edge(S, id, inf);
			if(num[i][j] == 2) add_edge(id, T, inf);
			for(int k = 0; k &amp;#x3C; 4; k++) {
				int nx = i + dir[k][0], ny = j + dir[k][1];
				if(nx &amp;#x3C; 1 || ny &amp;#x3C; 1 || nx &gt; n || ny &gt; m) continue;
				add_edge(id, get_id(nx, ny), 1);
			}
		}
	}
	cout &amp;#x3C;&amp;#x3C; Dinic() &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P4452】[国家集训队]航班安排</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4452</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4452</guid><description>算法题解</description><pubDate>Wed, 14 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;有  $K$ 架飞机，$N$ 个机场，其中 $1$ 号为基地机场。&lt;/p&gt;
&lt;p&gt;每天 $0$ 时刻，飞机才可以从基地机场起飞，并且不晚于 $T$ 时刻回到基地机场。&lt;/p&gt;
&lt;p&gt;对于 $\forall i, j$，题目给定 $t_{i, j}$ 表示从 $i$ &lt;strong&gt;空载&lt;/strong&gt;飞到 $j$ 需要的时间，$f_{i, j}$ 表示从 $i$ &lt;strong&gt;空载&lt;/strong&gt;飞到 $j$ 需要的费用。&lt;/p&gt;
&lt;p&gt;有 $M$ 个&lt;strong&gt;包机请求&lt;/strong&gt;，给定 $(a, b, s, t, c)$，表示在 $s$ 时刻从 $a$ 机场起飞，可以恰好在 $t$ 时刻到达 $b$ 机场，净获利 $c$.&lt;/p&gt;
&lt;p&gt;注意，包机请求的飞行时间为 $(t-s)\neq t_{a, b}$. 包机请求的费用为 $-c$，&lt;strong&gt;与 $f$ 不同&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;对请求进行拆点。&lt;strong&gt;起点向终点连容量为 $1$，价值为 $c$ 的边。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后处理时间限制。对于每个请求，如果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**可以在请求开始之前，从基地飞到起点：**从源点向请求起点连边，容量为 $\inf$，价值为对应的 $-f$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**可以在 $T$ 之前，从终点飞到基地：**从终点向汇点连边，容量为 $\inf$，价值为对应的 $-f$.&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;p&gt;处理 $k$ 架飞机。&lt;strong&gt;再建立一个源点&lt;/strong&gt;，向原来的源点连容量为 $k$，费用为 $0$ 的边即可。&lt;/p&gt;
&lt;p&gt;跑&lt;strong&gt;最大费用最大流&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;
const int MAXN = 408, MAXM = 1e6;
const int inf = 0x3f3f3f3f;
int n, m, k, ed;
int Tim[MAXN][MAXN], Cos[MAXN][MAXN];
int S, T;
struct Edge{
    int from, to, nxt, flow, val;
}edge[MAXM];
int head[MAXN], e_cnt = 1;
void add(int u, int v, int w, int c) {
    edge[++e_cnt].from = u;
    edge[e_cnt].to = v;
    edge[e_cnt].flow = w;
    edge[e_cnt].val = c;
    edge[e_cnt].nxt = head[u];
    head[u] = e_cnt;
}
void add_edge(int u, int v, int w, int c) {
    add(u, v, w, c);
    add(v, u, 0, -c);
}
struct In{int a, b, s, t, c;} in[MAXN];
int dis[MAXN], pre[MAXN];
bool vis[MAXN];
bool SPFA() {
    memset(dis, 0x3f, sizeof(dis)); dis[S] = 0;
    memset(vis, 0, sizeof(vis)); vis[S] = true;
    memset(pre, 0, sizeof(pre));
    queue&amp;#x3C;int&gt; q; q.push(S);
    while(!q.empty()) {
        int x = q.front(); q.pop(); vis[x] = false;
        for(int i = head[x]; i; i = edge[i].nxt) {
            int v = edge[i].to, flow = edge[i].flow, val = edge[i].val;
            if(flow &amp;#x26;&amp;#x26; dis[x] + val &amp;#x3C; dis[v]) {
                dis[v] = dis[x] + val;
                pre[v] = i;
                if(!vis[v]) {
                    q.push(v); vis[v] = true;
                }
            }
        }
    }
    return (pre[T] != 0);
}
pair&amp;#x3C;int, int&gt; MCMF() {
    int flow = 0, cost = 0;
    while(SPFA()) {
        int minflow = inf;
        for(int i = T; i != S; i = edge[pre[i]].from) {
            minflow = min(minflow, edge[pre[i]].flow);
        }
        flow += minflow;
        for(int i = T; i != S; i = edge[pre[i]].from) {
            edge[pre[i]].flow -= minflow;
            edge[pre[i] ^ 1].flow += minflow;
            cost += edge[pre[i]].val * minflow;
        }
    }
    return make_pair(flow, cost);
}
int main() {
    scanf(&quot;%d%d%d%d&quot;, &amp;#x26;n, &amp;#x26;m, &amp;#x26;k, &amp;#x26;ed);
    S = 0; T = 2 * m + 2;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        for(int j = 1; j &amp;#x3C;= n; j++) {
            scanf(&quot;%d&quot;, &amp;#x26;Tim[i][j]);
        }
    }
    for(int i = 1; i &amp;#x3C;= n; i++) {
        for(int j = 1; j &amp;#x3C;= n; j++) {
            scanf(&quot;%d&quot;, &amp;#x26;Cos[i][j]);
        }
    }
    add_edge(S, S + 1, k, 0);
    for(int i = 1; i &amp;#x3C;= m; i++) scanf(&quot;%d%d%d%d%d&quot;, &amp;#x26;in[i].a, &amp;#x26;in[i].b, &amp;#x26;in[i].s, &amp;#x26;in[i].t, &amp;#x26;in[i].c);
    for(int i = 1; i &amp;#x3C;= m; i++) {
        add_edge(i + 1, i + m + 1, 1, -in[i].c);
        if(in[i].t + Tim[in[i].b + 1][1] &amp;#x3C;= ed) add_edge(i + m + 1, T, inf, Cos[in[i].b + 1][1]);
        if(in[i].s - Tim[1][in[i].a + 1] &gt;= 0) add_edge(S + 1, i + 1, inf, Cos[1][in[i].a + 1]);
        for(int j = 1; j &amp;#x3C;= m; j++) {
            if(in[i].t + Tim[in[i].b + 1][in[j].a + 1] &amp;#x3C;= in[j].s) {
                add_edge(i + m + 1, j + 1, inf, Cos[in[i].b + 1][in[j].a + 1]);
            }
        }
    }
    cout &amp;#x3C;&amp;#x3C; -MCMF().second &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3698】[CQOI2017]小Q的棋盘</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3698</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3698</guid><description>算法题解</description><pubDate>Wed, 12 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;LuoGu - P3698 - [CQOI2017]小Q的棋盘&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个节点的有根树，定义一次移动为：从当前所在节点移动到一个相邻节点。问从根节点出发，移动 $k$ 次后，最多经过多少个不同的节点。&lt;/p&gt;
&lt;p&gt;每个节点可以被重复经过，但只计算一次。&lt;/p&gt;
&lt;p&gt;$n\leq 100$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;这道题可以用树形 DP 来做，这里只提供时间复杂度更优的&lt;strong&gt;贪心&lt;/strong&gt;做法。&lt;/p&gt;
&lt;p&gt;首先很容易想到，我们要尽可能保证每一次移动都到达一个新的节点，所以要先向深度最深的节点走。&lt;/p&gt;
&lt;p&gt;如果走的过程中，$k$ 次移动机会就用完了，答案即为 $k$.&lt;/p&gt;
&lt;p&gt;若否，我们要耗费尽可能少的移动次数，来到达一个新的节点。可以发现，在走完最长链之后，&lt;strong&gt;每到一个新节点都只需要 $2$ 步&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/05/12/4793df5361e84.png&quot; alt=&quot;Snipaste_2021-05-12_16-34-24.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示。先选择最长链 $1\to 2\to 5\to 9\to 12$. 此时如果要再添加点，只需要找一个&lt;strong&gt;与任意一个被选节点相邻&lt;/strong&gt;的节点即可。&lt;/p&gt;
&lt;p&gt;比如选择了点 $4$，那路径就变成：$1\to 2\to 4\to 2\to 5\to 9\to 12$.&lt;/p&gt;
&lt;p&gt;由于每次新选的点，都与一个路径中的点相邻，所以只要在路径中插入两步，一定可以走到这个新的节点。&lt;/p&gt;
&lt;p&gt;因此，在选完最长链之后，剩余的步数&lt;strong&gt;每两步都可以走到一个新的节点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;形式化地，设这棵树的节点个数为 $n$，最长链长度为 $l$，可用步数为 $k$，可以这样表示出答案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 $k\leq l - 1$，步数只够在最长链上移动，则答案为 $k$.&lt;/li&gt;
&lt;li&gt;若 $k &gt; l - 1$，走完最长链之后有剩余步数，则答案为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\max
\left{
\begin{array}{lr}
l + \left\lfloor{\frac{k-l+1}{2}}\right\rfloor\
n
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;这里注意，如果步数足够多，也最多只能走完所有点，此时答案为 $n$.&lt;/p&gt;
&lt;p&gt;时间复杂度为 $\mathcal{O}(n)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 108;
int n, k;
struct Edge{
    int to, nxt;
}edge[MAXN &amp;#x3C;&amp;#x3C; 1];
int head[MAXN], e_cnt = 0;
void add(int u, int v) {
    edge[++e_cnt].to = v;
    edge[e_cnt].nxt = head[u];
    head[u] = e_cnt;
}
int deepest = 1;//最长链的长度
void dfs(int x, int fa, int dep) {
    deepest = max(deepest, dep);
    for(int i = head[x]; i; i = edge[i].nxt) {
        int v = edge[i].to; if(v == fa) continue;
        dfs(v, x, dep + 1);
    }
}
int main() {
    scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;k);
    for(int i = 1; i &amp;#x3C; n; i++) {
        int a, b; scanf(&quot;%d%d&quot;, &amp;#x26;a, &amp;#x26;b); a++, b++;
        add(a, b); add(b, a);
    }
    dfs(1, 0, 1);
    if(deepest &gt;= k + 1) cout &amp;#x3C;&amp;#x3C; k + 1 &amp;#x3C;&amp;#x3C; endl;
    else cout &amp;#x3C;&amp;#x3C; min(deepest + (k - deepest + 1) / 2, n);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P4562】[JXOI2018]游戏</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4562</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4562</guid><description>算法题解</description><pubDate>Wed, 12 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $[l, r]$ 区间，对于区间内所有数的一个顺序，每次取出最前面的数并将其倍数划去，当所有区间的数都被划去时停止。求所有排列中，取出的数的个数之和。&lt;/p&gt;
&lt;p&gt;$1\leq l\leq r\leq 10^7$，答案对 $10^9+7$ 取模。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;对于每个顺序，有一些特殊的数必须被取，并且全部取完这些之后就不用再取了。&lt;/p&gt;
&lt;p&gt;可以发现这些数的特征是：&lt;strong&gt;除了自己外，没有因子在 $[l, r]$ 内&lt;/strong&gt;。由于没有因子在 $[l, r]$ 内，这些数不可能被提前划去。&lt;/p&gt;
&lt;p&gt;回到题目对 $t(p)$ 的定义，可以知道对于任意排列 $p$，$t(p)$ 的数值，就是顺序中&lt;strong&gt;最后&lt;/strong&gt;一个这样特殊的数&lt;strong&gt;的位置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如：在对于 $[3, 8]$ 的顺序 $5, 3, 8, 6, 7, 4$ 当中，$4$ 就是其中的一个关键数。$4$ 不能被区间内的其他数提前划掉，这个顺序的 $t(p)=6$.&lt;/p&gt;
&lt;p&gt;先考虑如何快速找出这些数。&lt;/p&gt;
&lt;p&gt;我们可以用线性筛法，预处理出每个数的最小值因子 $\min_x$，这样用 $\dfrac{x}{\min_x}$ 就可以求出 $x$ 的最大因子 $\max_x$. 如果 $\max_x &amp;#x3C; l$，那么 $x$ 就是特殊的数。&lt;/p&gt;
&lt;p&gt;此时我们已经知道了特殊的数有哪些，考虑如何直接算出答案。&lt;/p&gt;
&lt;p&gt;设一共有 $s$ 个特殊的数，则答案为：
$$
\sum_{i=s}^{n}{i\times \dbinom{i-1}{s - 1}\times (s-1)!\times s\times (n-s)!}
$$
可以理解为，枚举最后一个关键数的位置，统计其贡献：若有 $x$ 个排列使得最后一个关键数的位置为 $i$，那么其对答案的贡献为 $i\times x$.&lt;/p&gt;
&lt;p&gt;现在要求的就是这个 $x$.&lt;/p&gt;
&lt;p&gt;首先，我们要在 $i$ 这个位置放一个关键数，这个数可以在关键数中随便取，方案数为 $s$.&lt;/p&gt;
&lt;p&gt;$\tbinom{i-1}{s-1}$ 是说，在前 $i-1$ 个位置放进去 $s-1$ 个关键数，这 $s-1$ 个数的顺序随意，所以还要乘上一个 $(s-1)!$.&lt;/p&gt;
&lt;p&gt;对剩下没放的 $n-s$ 个数，可以在所有空余的地方随便放，方案数为 $(n-s)!$.&lt;/p&gt;
&lt;p&gt;于是我们得到上面的答案，预处理组合数后，循环一次求解即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 1e7 + 10, mod = 1e9 + 7;
int l, r;
bool is_prime[MAXN];
int min_div[MAXN], prime[MAXN], cnt = 0;//min_div[x] 为x的最小非1因子
int fac[MAXN], inv_fac[MAXN];
inline int power(int x, int k, int mod) { //快速幂
	int res = 1;
	while(k) {
		if(k &amp;#x26; 1) {
			res = res * x % mod;
		}
		x = x * x % mod;
		k &gt;&gt;= 1;
	}
	return res;
}
inline void init() { //预处理组合数（阶乘 &amp;#x26; 阶乘的逆元）
	fac[0] = 1;
	for(int i = 1; i &amp;#x3C;= r; i++) {
		fac[i] = fac[i - 1] * i % mod;
	}
	inv_fac[r] = power(fac[r], mod - 2, mod);
	for(int i = r - 1; i &gt;= 0; i--) {
		inv_fac[i] = inv_fac[i + 1] * (i + 1) % mod;
	}
}
inline int C(int n, int m) { //求组合数，C(n, m) = C_n^m
	return fac[n] * inv_fac[m] % mod * inv_fac[n - m] % mod;
}
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;l, &amp;#x26;r);
	int n = r - l + 1;
	init();
	if(l == 1) { //这里需要特判，因为下面的因子并不包括1，对于l=1的情况容易直接得出答案
		cout &amp;#x3C;&amp;#x3C; fac[n] * (n + 1) % mod * 500000004 % mod;
		return 0;
	}
	for(int i = 2; i &amp;#x3C;= r; i++) is_prime[i] = 1;
	for(int i = 2; i &amp;#x3C;= r; i++) { //线性筛，求每个数的最小因子
		if(is_prime[i]) {
			prime[++cnt] = i; min_div[i] = i;
		}
		for(int j = 1; j &amp;#x3C;= cnt &amp;#x26;&amp;#x26; i * prime[j] &amp;#x3C;= r; j++) {
			is_prime[i * prime[j]] = false;
			min_div[i * prime[j]] = prime[j];
			if(i % prime[j] == 0) break;
		}
	}
	int sum = 0, ans = 0;
	for(int i = l; i &amp;#x3C;= r; i++) sum += (i / min_div[i] &amp;#x3C; l); //求特殊数的个数
	for(int i = sum; i &amp;#x3C;= n; i++) { //求解答案
		ans += i * C(i - 1, sum - 1) % mod * fac[sum] % mod * fac[n - sum] % mod;
		ans %= mod;
	}
	cout &amp;#x3C;&amp;#x3C; ans % mod &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P4079】[SDOI2016]齿轮</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4079</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4079</guid><description>算法题解</description><pubDate>Tue, 11 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $n$ 个齿轮和 个 $m$ 个链条，每个链条连接两个齿轮 $u, v$，并且有一个传动比。&lt;/p&gt;
&lt;p&gt;传动比有两个参数 $x, y$，表示在&lt;strong&gt;单位时间内&lt;/strong&gt;，若 $u$ 转动 $x$ 圈，则 $v$ 必须转动 $y$ 圈。&lt;/p&gt;
&lt;p&gt;问 $n$ 个齿轮能否同时转动。&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;p&gt;题目卡精度，所以在固定第一个点的转速时，可以定为 $100$ 或 $1000$，运算过程中比较大小需要用到 $\epsilon$ 解决精度问题。BJEA123456&lt;/p&gt;
&lt;p&gt;具体实现上，只需要循环一遍，对每个点都 DFS 一遍，判断是否产生了矛盾就可以了。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 1e3 + 10, MAXM = 2e5 + 10;
const double eps = 1e-5;
int T, n, m;
struct Edge{
    int to, nxt, x, y;
}edge[MAXM];
int head[MAXN], cnt = 0;
void add(int u, int v, int x, int y) {
    edge[++cnt].to = v;
    edge[cnt].x = x;
    edge[cnt].y = y;
    edge[cnt].nxt = head[u];
    head[u] = cnt;
}
double vel[MAXN];
bool dfs(int x) {
    for(int i = head[x]; i; i = edge[i].nxt) {
        int v = edge[i].to;
        if(fabs(vel[v]) &amp;#x3C; eps) {// if(vel[v] == 0)
            vel[v] = vel[x] * (edge[i].y) / (double)(edge[i].x);
            if(!dfs(v)) return false;
        }
        if(fabs(vel[v] - vel[x] * (edge[i].y) / (double)(edge[i].x)) &gt; eps) 
            // if(vel[v] != vel[x] * edge[i].y / edge[i].x)
            return false;
    }
    return true;
}
int main() {
    scanf(&quot;%d&quot;, &amp;#x26;T);
    for(int t = 1; t &amp;#x3C;= T; t++) {
        scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;m);
        memset(head, 0, sizeof(head)); cnt = 0;
        memset(vel, 0, sizeof(vel));
        for(int i = 1; i &amp;#x3C;= m; i++) {
            int a, b, x, y; scanf(&quot;%d%d%d%d&quot;, &amp;#x26;a, &amp;#x26;b, &amp;#x26;x, &amp;#x26;y);
            add(a, b, x, y); add(b, a, y, x);//注意 反向边的转速比也要反过来
        }
        bool ok = true;
        for(int i = 1; i &amp;#x3C;= n; i++) {
            if(fabs(vel[i]) &amp;#x3C; eps) {
                vel[i] = 100;
                if(!dfs(i)) {
                    ok = false; break;
                }
            }
        }
        printf(&quot;Case #%d: %s\n&quot;, t, (ok)?&quot;Yes&quot;:&quot;No&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P4171】[JSOI2010]满汉全席</title><link>https://www.tonyyin0418.com/blog/oi-solution/p4171</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p4171</guid><description>算法题解</description><pubDate>Tue, 11 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;题面比较长，先概括一下。如果感觉题目没有完全看懂，建议先看一下下面的要点概括。&lt;/p&gt;
&lt;p&gt;题目给定了 $n$ 种&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;题目给定了 $m$ 条限制，每条限制包含&lt;strong&gt;两个&lt;/strong&gt;菜品的信息。对于每条限制，限制被满足，当且仅当：在给出的两个菜品中，至少有一个菜品被制作出来。&lt;/p&gt;
&lt;p&gt;题目问：是否存在一种菜品制作方法，使得每条限制都被满足。&lt;/p&gt;
&lt;p&gt;题目有多组询问，询问次数 $T\leq 50$，食材个数 $n\leq 100$，限制条数 $m\leq 1000$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;看到每种食材必须在两种制作方法中，恰好选择一种，想到 &lt;strong&gt;2-SAT&lt;/strong&gt; 算法。&lt;/p&gt;
&lt;p&gt;对于每种食材，将两种制作方式拆成两个点。具体地，代表满式做法的点编号为 $[1, n]$，代表汉式做法的点编号为 $[n + 1, 2n]$.&lt;/p&gt;
&lt;p&gt;因为要用到 &lt;strong&gt;2-SAT&lt;/strong&gt;，我们需要找到&lt;strong&gt;形如 $a \land b = 0$ 且 $a \lor b = 1$ 这样&lt;/strong&gt; 的关系。&lt;/p&gt;
&lt;p&gt;对于限制条件给定的两个菜品 $u, v$，通过分类讨论，可以找到其中这样的关系。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;限制内容：&lt;strong&gt;$u$ 为满式，$v$ 为满式&lt;/strong&gt;。关系：
&lt;ul&gt;
&lt;li&gt;若 $u$ 为汉式，则 $v$ 必为满式；&lt;/li&gt;
&lt;li&gt;若 $v$ 为汉式，则 $u$ 必为汉式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;限制内容：&lt;strong&gt;$u$ 为满式，$v$ 为汉式&lt;/strong&gt;。关系：
&lt;ul&gt;
&lt;li&gt;若 $u$ 为汉式，则 $v$ 必为汉式；&lt;/li&gt;
&lt;li&gt;若 $v$ 为满式，则 $u$ 必为满式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;限制内容：&lt;strong&gt;$u$ 为汉式，$v$ 为满式&lt;/strong&gt;。关系：
&lt;ul&gt;
&lt;li&gt;若 $u$ 为满式，则 $v$ 必为满式；&lt;/li&gt;
&lt;li&gt;若 $v$ 为汉式，则 $u$ 必为汉式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;限制内容：&lt;strong&gt;$u$ 为汉式，$v$ 为汉式&lt;/strong&gt;。关系：
&lt;ul&gt;
&lt;li&gt;若 $u$ 为满式，则 $v$ 必为汉式；&lt;/li&gt;
&lt;li&gt;若 $v$ 为满式，则 $u$ 必为汉式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样建图之后，直接跑 &lt;strong&gt;2-SAT&lt;/strong&gt; 即可。如果你不会 &lt;strong&gt;2-SAT&lt;/strong&gt;，可以参考：&lt;a href=&quot;https://anguei.blog.luogu.org/solution-p4782&quot;&gt;Anguei 的博客 - 2-SAT 问题&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;为了缩短长度，代码中删除了 $\rm{I/O}$ 部分，自行补上即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
/*
折叠了快速读入和输出部分
*/
const int MAXN = 1e6 + 10, MAXM = 1e6 + 10;
int n, m;
struct Edge{
	int to, nxt;
}edge[MAXM &amp;#x3C;&amp;#x3C; 1];
int head[MAXN &amp;#x3C;&amp;#x3C; 1], e_cnt = 0;
void add_edge(int u, int v) {
	edge[++e_cnt].to = v;
	edge[e_cnt].nxt = head[u];
	head[u] = e_cnt;
}
int low[MAXN &amp;#x3C;&amp;#x3C; 1], dfn[MAXN &amp;#x3C;&amp;#x3C; 1], tot = 0, st[MAXN &amp;#x3C;&amp;#x3C; 1], top = 0, vis[MAXN &amp;#x3C;&amp;#x3C; 1];
int col[MAXN &amp;#x3C;&amp;#x3C; 1], scc = 0;
void Tarjan(int x) {
	low[x] = dfn[x] = ++tot;
	st[++top] = x; vis[x] = true;
	for(int i = head[x]; i; i = edge[i].nxt) {
		int v = edge[i].to;
		if(!dfn[v]) {
			Tarjan(v); low[x] = min(low[x], low[v]);
		} else if(vis[v]) {
			low[x] = min(low[x], dfn[v]);
		}
	}
	if(dfn[x] == low[x]) {
		col[x] = ++scc;
		int y;
		do {
			y = st[top--]; vis[y] = false;
			col[y] = scc;
		} while(x != y);
	}
}
int main() {
	n = read(); m = read();
	for(int i = 1; i &amp;#x3C;= m; i++) {
		int a = read(), va = read(), b = read(), vb = read(); 
		//a或b：连边方式为 (a-&gt;非b), (b-&gt;非a)
		add_edge(a + n * (va &amp;#x26; 1), b + n * (vb ^ 1));
		add_edge(b + n * (vb &amp;#x26; 1), a + n * (va ^ 1));
	}
	for(int i = 1; i &amp;#x3C;= (n &amp;#x3C;&amp;#x3C; 1); i++) {
		if(!dfn[i]) Tarjan(i);
	}
	for(int i = 1; i &amp;#x3C;= (n &amp;#x3C;&amp;#x3C; 1); i++) {
		if(col[i] == col[i + n]) {
			cout &amp;#x3C;&amp;#x3C; &quot;IMPOSSIBLE\n&quot; &amp;#x3C;&amp;#x3C; endl;
			return 0;
		}
	}
	cout &amp;#x3C;&amp;#x3C; &quot;POSSIBLE\n&quot;;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		out(col[i] &amp;#x3C; col[i + n]); putchar(&apos; &apos;);
	}
	putchar(&apos;\n&apos;);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>2021年五一假期 省选真题分享</title><link>https://www.tonyyin0418.com/blog/oi-solution/pre-20210504</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/pre-20210504</guid><description>2021年五一假期，校内集训的省选真题分享讲稿</description><pubDate>Tue, 04 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;P4071 - SDOI2016 - D2T1 - 排列计数&lt;/h1&gt;
&lt;h2&gt;题目信息&lt;/h2&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;求有多少种 $1$ 到 $n$ 的排列 $a$，满足序列恰好有 $m$ 个位置 $i$，使得 $a_i=i$.&lt;/p&gt;
&lt;p&gt;答案对 $10^9+7$ 取模，多组询问，询问次数为 $T$.&lt;/p&gt;
&lt;p&gt;| 测试点编号 |  $T=$  | $n, m\leq$ |  测试点编号  |      $T=$      | $n, m\leq$ |
| :--------: | :----: | :--------: | :----------: | :------------: | :--------: |
| $1 \sim 3$ | $10^3$ |    $8$     | $10 \sim 12$ |     $10^3$     |   $10^3$   |
| $4 \sim 6$ | $10^3$ |    $12$    | $13 \sim 14$ | $5\times 10^5$ |   $10^3$   |
| $7 \sim 9$ | $10^3$ |   $100$    | $15 \sim 20$ | $5\times 10^5$ |   $10^6$   |&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$0\leq m\leq 10^6$.&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $\rm{1}$&lt;/h2&gt;
&lt;p&gt;$T=10^3$，$n, m\leq 8$.&lt;/p&gt;
&lt;p&gt;对于每次询问，暴力枚举所有排列，时间复杂度 $\mathcal{O}(T\times n!)$，期望得分 $\rm{15pts}$.&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $\rm{2}$&lt;/h2&gt;
&lt;p&gt;想到&lt;a href=&quot;#cuopai&quot;&gt;错排&lt;/a&gt;之后，正解非常自然。&lt;/p&gt;
&lt;p&gt;根据小学排列组合知识，我们从数列中任意取 $n$ 个数，让它们满足 $a_i=i$，情况数为 $C_{n}^{m}$.&lt;/p&gt;
&lt;p&gt;又因为 $n$ 个数的排列中，有 $m$ 个位置满足 $a_i=i$，对于剩下的 $n-m$ 个位置需要满足 $a_i\neq i$.&lt;/p&gt;
&lt;p&gt;这样的情况数是 $n-m$ 的错排数，也就是 $D_{n-m}$.&lt;/p&gt;
&lt;p&gt;由于乘法原理，最终的答案就是 $C_{n}^m\times D_{n-m}$.&lt;/p&gt;
&lt;p&gt;对于多组询问，我们需要 $\mathcal{O}(n)$ &lt;a href=&quot;#zuheshu&quot;&gt;预处理组合数&lt;/a&gt;和错排数。&lt;/p&gt;
&lt;h2&gt;错排&lt;/h2&gt;
&lt;h3&gt;概念&lt;/h3&gt;
&lt;p&gt;对于一个 $n$ 个元素的排列，若一个排列中所有的元素都不在自己的位置上，这样的一个排列称为一个错排。&lt;/p&gt;
&lt;p&gt;一般将 $n$ 个元素的错排数量记作 $D_n$.&lt;/p&gt;
&lt;p&gt;通过递推，可以在 $\mathcal{O}(n)$ 的复杂度内处理出 $D_1,D_2\cdots, D_n$.&lt;/p&gt;
&lt;h3&gt;递推式&lt;/h3&gt;
&lt;p&gt;推导过程不唯一。&lt;/p&gt;
&lt;p&gt;第一步，在 $[1, n-1]$ 中任取一个元素 $i$，将 $n$ 连向 $i$，方法数量为 $(n-1)$.&lt;/p&gt;
&lt;p&gt;第二步，确定 $i$ 连向哪个元素，分类讨论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$i$ 连向 $n$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此时剩下的 $n-2$ 个元素要满足错排，对答案贡献为 $(n-1)\times D_{n-2}$.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;$i$ 不连向 $n$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此时，相当于有 $n-1$ 个元素需要满足错排，对答案贡献为 $(n-1)\times D_{n-1}$.&lt;/p&gt;
&lt;p&gt;根据加法原理，得到递推式：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\begin{align}
D_{n}=(n-1)\times(D_{n-1}+D_{n-2})\tag{n&gt;2}
\end{align}
$$&lt;/p&gt;
&lt;p&gt;​		特殊地，$D_1=0$，$D_2=1$.&lt;/p&gt;
&lt;h2&gt;求逆元 &amp;#x26; 组合数&lt;/h2&gt;
&lt;h3&gt;扩展欧几里得&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;扩展欧几里得算法是用来在已知 $a,b$ 的情况下求解一组 $x,y$，使满足 $ax+by=gcd(a,,b)=d$.&lt;/p&gt;
&lt;p&gt;$\gcd(a, b)$ 为 $a, b$ 的最大公因数，方程&lt;strong&gt;一定有整数解&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;特解&lt;/h4&gt;
&lt;p&gt;要计算 $\gcd(a,,b)$，并求出满足条件的 $x,y$.&lt;/p&gt;
&lt;p&gt;将下一个状态 $b$ 和 $(a%b)$ 表示为：$b\cdot x_1+(a%b)\cdot y_1=d$&lt;/p&gt;
&lt;p&gt;则：
$$
\begin{align}
\because b\cdot x_1+(a%b)\cdot y_1=d,\
\text{又}\because a%b=a-\left\lfloor\frac{a}{b}\right\rfloor\times b\
\end{align}
$$
$$
\begin{aligned}\therefore gcd(a,,b)=d&amp;#x26;=b\cdot x_1+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\cdot y_1\
&amp;#x26;=b\cdot x_1+a\cdot y_1-\left\lfloor\frac{a}{b}\right\rfloor\times b\times y_1\
&amp;#x26;=a\times y1+b\times(x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;所以满足条件的 $x=y_1,,y=x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int exgcd(int a, int b, int &amp;#x26;x, int &amp;#x26;y) {//函数返回值为gcd(a,b)
    if(b == 0) {
        x = 1;//gcd(a,b) = a = 1a+0b
        y = 0;
        return a;
    }
    int r = exgcd(b, a % b, x, y);//先递归
    int t = x;//再计算x,y
    x = y;
    y = t - a / b * y;
    return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度为：$\mathcal{O}(\min(\log a, \log b))$，即 $\mathcal{O}(\log n)$ 级别。&lt;/p&gt;
&lt;h4&gt;通解&lt;/h4&gt;
&lt;p&gt;假设特解为 $(x_0,y_0)$，满足 $ax_0+by_0=gcd(a,,b)=d$，&lt;/p&gt;
&lt;p&gt;则有：
$$
a(x_0+k\frac{b}{d})+b(y_0-k\frac{a}{d})=d\tag{k为整数}
$$
所以方程的通解为：
$$
\begin{aligned}
x&amp;#x26;=x_0+k\frac{b}{d}\
y&amp;#x26;=y_0-k\frac{a}{d}
\end{aligned}
$$
对于其它二元一次不定方程$ax+by=c$（$c$为任意正整数）&lt;strong&gt;只有当 $d\mid c$ 时才有整数解&lt;/strong&gt;，通解形式：
$$
\begin{aligned}
x&amp;#x26;=\frac{c}{d}x_0+k\frac{b}{d}\
y&amp;#x26;=\frac{c}{d}y_0-k\frac{a}{d}
\end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;同余方程（模方程）&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;指形如 $ax\equiv b\pmod c$ 的一种方程。其中 $a,b,c$ 是参数，$a,c$ 是正整数，$b$ 是小于 $c$ 的非负整数，$ x$ 是未知数。&lt;/p&gt;
&lt;h4&gt;解同余方程&lt;/h4&gt;
&lt;p&gt;根据同余定义，$ax\equiv b\pmod c$ 可化为 $xa+kc=b$.  其中 $a,b,c$ 已知，$x,k$ 未知，所以能用&lt;strong&gt;扩展欧几里得解同余方程&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;通解&lt;/h4&gt;
&lt;p&gt;如果 $b\nmid gcd(a,c)$，方程无整数解.&lt;/p&gt;
&lt;p&gt;否则同扩展欧几里得酸法，求得通解：
$$
\begin{aligned}
x&amp;#x26;=\frac{b}{d}x_0+k\frac{c}{d}\
k&amp;#x26;=\frac{b}{d}p_0-k\frac{a}{d}
\end{aligned}
$$
其中 $k\in \Bbb{Z}$.&lt;/p&gt;
&lt;h4&gt;最小非负整数解&lt;/h4&gt;
&lt;p&gt;因为 $x=\frac{b}{d}x_0+k\frac{c}{d}$，其中 $x_0$ 是方程 $xa+kc=gcd(a,,c)$ 的特解， $\frac{b}{d}x_0$ 是原同余方程的特解，&lt;/p&gt;
&lt;p&gt;若 $x&gt;0$，则有 $x\equiv \frac{b}{d}x_0 \pmod{ \frac{c}{d}} $。最小非负整数解为 $\frac{b}{d}x_0,%,\frac{c}{d}$.&lt;/p&gt;
&lt;p&gt;若 $x&amp;#x3C;0$，同理，最小非负整数解为 $(\frac{b}{d}x_0\bmod \frac{c}{d}+\frac{c}{d} )\bmod\frac{c}{d}$.&lt;/p&gt;
&lt;p&gt;综上，最小非负整数解为：
$$
x=(\frac{b}{d}x_0\bmod\frac{c}{d}+\frac{c}{d})\bmod \frac{c}{d}
$$&lt;/p&gt;
&lt;h3&gt;乘法逆元&lt;/h3&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;给定两个整数 $a$ 和 $p$，假设存在 $x$ 使得 $ax\equiv 1\pmod p)$，称 $x$ 为 $a$ 关于 $p$ 的&lt;strong&gt;乘法逆元&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$a$ 关于 $p$ 的逆元存在的&lt;strong&gt;充分必要条件&lt;/strong&gt;是 $\gcd(a,p)=1$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在模意义下，乘一个数 $x$ 的逆元，可以理解为除以 $x$&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;求一个数的逆元&lt;/h4&gt;
&lt;p&gt;将上面式子变形，变成 $ax+kp=1$，用扩展欧几里得能够求出一个 $x$.&lt;/p&gt;
&lt;p&gt;如果 $p$ 是质数，有另一种方法求 $a$ 关于 $p$ 的逆元（费马小定理）
$$
a^{p-1}\bmod p=(a^{p-2}\times a)\bmod p=(x\times a)\bmod p=1
$$
所以 $a$ 关于 $p$ 的逆元是 $a^{b-2}\bmod b$ 的值，用快速幂优化。&lt;/p&gt;
&lt;p&gt;时间复杂度：均为 $\mathcal{O}(\log n)$ 级别。&lt;/p&gt;
&lt;h4&gt;线性预处理逆元&lt;/h4&gt;
&lt;p&gt;在 $\mathcal{O}(n)$ 的时间复杂度内，&lt;strong&gt;求出任意 $n$ 个数的逆元&lt;/strong&gt;，一般用于处理 $1\sim n$ 的逆元。&lt;/p&gt;
&lt;p&gt;设我们要处理出 $a_1, a_2\cdots, a_n$ 的逆元。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第一步，预处理出前缀积数组 $\operatorname{pre}&lt;em&gt;i=\prod&lt;/em&gt;{j=1}^ia_i$.&lt;/p&gt;
&lt;p&gt;第二步，求出 $\operatorname{pre}_n$ 的逆元 $\operatorname{pre_inv}_n$，复杂度 $\mathcal{O}(\log n)$.&lt;/p&gt;
&lt;p&gt;第三步，通过 $\operatorname{pre_inv}&lt;em&gt;i$，求出 $\operatorname{pre_inv}&lt;/em&gt;{i-1}$.&lt;/p&gt;
&lt;p&gt;在模意义下，$\operatorname{pre_inv}&lt;em&gt;i=\frac{1}{a_1\times a&lt;/em&gt;{2}\times \cdots \times a_{i}}$，$\operatorname{pre_inv}&lt;em&gt;{i-1}=\frac{1}{a_1\times a_2\times\cdots\times a&lt;/em&gt;{i-1}}=\operatorname{pre_inv}_{i}\times a_i$.&lt;/p&gt;
&lt;p&gt;第四步，通过 $\operatorname{pre_inv}_i$，求出 $\operatorname{inv}_i$.&lt;/p&gt;
&lt;p&gt;在模意义下，$\operatorname{inv}&lt;em&gt;i=\operatorname{pre}&lt;/em&gt;{i-1}\times \operatorname{pre_inv}_{i}$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;在上面的步骤中，$\operatorname{pre_inv}_i$ 即为 $i$ 的阶乘的逆元，在处理组合数时常用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有另一种方法也可以处理逆元，时间复杂度相同，但不好记，而且证明麻烦，不需要掌握。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;inv[1] = 1;
for (int i = 2; i &amp;#x3C;= n; ++i) {
    inv[i] = (p - p / i) * inv[p % i] % p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;预处理组合数&lt;/h3&gt;
&lt;p&gt;预处理后，$\mathcal{O}(1)$ 地求解 $[1, n]$ 范围内的组合数，对大质数取模的结果。&lt;/p&gt;
&lt;p&gt;记 $\operatorname{fac}_n=n!$，根据组合数的定义式：
$$
C_n^m=\frac{n!}{m!(n-m)!}=\frac{\operatorname{fac}_n}{\operatorname{fac}&lt;em&gt;m\times \operatorname{fac}&lt;/em&gt;{n-m}}
$$
可以得到：
$$
C_n^m \bmod p = \operatorname{fac}_n (\operatorname{fac}&lt;em&gt;m)^{-1}(\operatorname{fac}&lt;/em&gt;{n-m})^{-1}
$$
所以预处理出 $[1, n]$ 的阶乘，以及阶乘的逆元，每次即可 $\mathcal{O}(1)$ 求组合数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int fac[], inv_fac[];
fac[0] = 1;
for(int i = 1; i &amp;#x3C;= n; i++)
	fac[i] = fac[i - 1] * i % p;
inv_fac[n] = power(fac[n], p - 1);
for(int i = n - 1; i &gt;= 0; i--)
	inv_fac[i] = inv_fac[i + 1] * (i + 1) % p;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;P4343 - SHOI2015 - 自动刷题机&lt;/h1&gt;
&lt;h2&gt;题目信息&lt;/h2&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;/p&gt;
&lt;ol&gt;
&lt;li&gt;写了 $x$ 行代码&lt;/li&gt;
&lt;li&gt;心情不好，删掉了之前写的 $y$ 行代码。（如果 $y$ 大于当前代码长度则相当于全部删除。）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于一个 OJ，存在某个固定的正整数长度 $n$，一旦自动刷题机在某秒结束时积累了大于等于 $n$ 行的代码，它就会自动提交并 AC 此题，然后新建一个文件（即弃置之前的所有代码）并开始写下一题。SHTSC 在某个 OJ 上跑了一天的自动刷题机，得到了很多条关于写代码的日志信息。他突然发现自己没有记录这个 OJ 的 $n$ 究竟是多少。所幸他通过自己在 OJ 上的 Rank 知道了自动刷题机一共切了 $k$ 道题，希望你计算 $n$ 可能的最小值和最大值。&lt;/p&gt;
&lt;p&gt;题目给定 $l$ 条刷题日志，每条有一个整数 $x_i$ 表示添加的代码行数。&lt;/p&gt;
&lt;p&gt;$1\leq l\leq 10^5, -10^9\leq x_i\leq 10^9$.&lt;/p&gt;</content:encoded></item><item><title>【2021计蒜之道】wy的单机游戏</title><link>https://www.tonyyin0418.com/blog/oi-solution/2021jsk-d3-t3</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/2021jsk-d3-t3</guid><description>算法题解</description><pubDate>Sat, 01 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nanti.jisuanke.com/t/54138&quot;&gt;2021计蒜之道 - 预赛 - wy的单机游戏&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;给定一个 $n$ 行 $m$ 列的矩阵，矩阵中可能有三种字符：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt; 表示墙，不能移动到墙上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&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;上、下、左、右&lt;/strong&gt;移动一格，不能移动到墙上，也&lt;strong&gt;不能呆在原地不动&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在行走过程中，玩家如果走到了传送门上，无法选择是否传送，必然在一瞬间被传送到另一个与其配对的传送门处。&lt;/p&gt;
&lt;p&gt;设两个配对的传送门 $A$ 分别为 $A_1$ 和 $A_2$，则当玩家踏入 $A_1$ 的瞬间，就会被传送到 $A_2$ 处，这个过程不消耗时间。但在传送到 $A_2$ 后，$A_2$ 传送门会失效一秒钟，不能直接回到传送门 $A_1$.&lt;/p&gt;
&lt;p&gt;每次游戏，玩家出生在 $(1, 1)$，需要到达终点 $(n, m)$，求最少花费时间。&lt;/p&gt;
&lt;p&gt;$1\leq n, m\leq 100$.&lt;/p&gt;
&lt;h2&gt;题目解释&lt;/h2&gt;
&lt;p&gt;例如下面的地图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.#.
A#A
.#.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;玩家从 $(1, 1)$ 走到 $(3, 1)$ 的其中一种合法方案为：
$$
(1, 1)\rightarrow (2, 1)\rightarrow(2, 3)\rightarrow(1, 3)\rightarrow(2, 3)\rightarrow(2, 1)\rightarrow(3, 1)
$$
而这是一种不合法的方案：
$$
(1, 1)\rightarrow(2, 1)\rightarrow(3, 1)
$$
因为碰到传送门必定被传送走。&lt;/p&gt;
&lt;h2&gt;题目分析&lt;/h2&gt;
&lt;p&gt;不难想到最短路，考虑如何建图。&lt;/p&gt;
&lt;p&gt;要特殊处理的地方是：穿过传送门后的一秒。在这一秒，不能走回原来的传送门，只能走向其他点，而下一秒还可以再回来。&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;/p&gt;
&lt;p&gt;&lt;strong&gt;形式化地：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不妨设一对传送门分别为 $A_1$ 和 $A_2$，将所有传送门节点复制到第二层，复制后的节点叫 $B_1$ 和 $B_2$，传送门 $A_1$ 与空节点 $x$ 相邻。则：&lt;/p&gt;
&lt;p&gt;传送门之间连边的方式为 $A_1\rightarrow B_2$，$A_2\rightarrow B_1$.&lt;/p&gt;
&lt;p&gt;传送门与相邻节点之间连边的方式为：$x\rightarrow A_1$，$B_1\rightarrow x$.&lt;/p&gt;
&lt;p&gt;这样建图之后，点数为最多为 $n^2+26\times 2$，边数也大概在 $n^2$ 量级。这样用堆优化的 &lt;code&gt;Dijkstra&lt;/code&gt; 求出最短路即可。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 108;
int n, m;
char in[MAXN][MAXN];
int get(int x, int y) {//对坐标(x, y)做hash
    return (x - 1) * m + y;
}
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
struct Edge{
    int to, nxt, val;
}edge[(MAXN * MAXN) &amp;#x3C;&amp;#x3C; 4];
int head[(MAXN * MAXN) &amp;#x3C;&amp;#x3C; 1], e_cnt = 0;
void add(int u, int v, int w) {
    edge[++e_cnt].to = v;
    edge[e_cnt].val = w;
    edge[e_cnt].nxt = head[u];
    head[u] = e_cnt;
}
pair&amp;#x3C;int, int&gt; door[30][2];
//door[i][0]存储的是，第i组传送门的第一个点的坐标，door[i][1]是第二个点的坐标
int dis[(MAXN * MAXN) &amp;#x3C;&amp;#x3C; 1];
bool vis[(MAXN * MAXN) &amp;#x3C;&amp;#x3C; 1];
priority_queue&amp;#x3C;pair&amp;#x3C;int, int&gt;, vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt;, greater&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt; &gt; q;
void Dijkstra() {
    memset(dis, 0x3f, sizeof(dis));
    dis[1] = 0;
    q.push(make_pair(0, 1));
    while(!q.empty()) {
        int id = q.top().second; q.pop();
        if(vis[id]) continue;
        vis[id] = true;
        for(int i = head[id]; i; i = edge[i].nxt) {
            int ne = edge[i].to, val = edge[i].val;
            if(dis[ne] &gt; dis[id] + val) {
                dis[ne] = dis[id] + val;
                q.push(make_pair(dis[ne], ne));
            }
        }
    }
}
signed main() {
    scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;m);
    for(int i = 1; i &amp;#x3C;= n; i++) {
        cin &gt;&gt; (in[i] + 1);
    }
    //处理传送门：
    for(int i = 1; i &amp;#x3C;= n; i++) {
        for(int j = 1; j &amp;#x3C;= m; j++) {
            if(in[i][j] &gt;= &apos;A&apos; &amp;#x26;&amp;#x26; in[i][j] &amp;#x3C;= &apos;Z&apos;) {
                int num = in[i][j] - &apos;A&apos; + 1;
                if(door[num][0].first == 0) {//第一次遇到
                    door[num][0] = make_pair(i, j);
                } else {//第二次遇到，传送点之间跨层连边，即A1向B2连边，A2向B1连边
                    add(get(i, j), get(door[num][0].first, door[num][0].second) + n * m, 0);
                    add(get(door[num][0].first, door[num][0].second), get(i, j) + n * m, 0);
                }
            }
        }
    }
    //处理空白点相关：
    for(int i = 1; i &amp;#x3C;= n; i++) {
        for(int j = 1; j &amp;#x3C;= m; j++) {
            if(in[i][j] == &apos;#&apos;) continue;
            else if(in[i][j] == &apos;.&apos;) {//对于空白点，可以向相邻的，第一层传送门/空白点连边
                for(int k = 0; k &amp;#x3C; 4; k++) {
                    int nx = i + dir[k][0], ny = j + dir[k][1];
                    if(nx &amp;#x3C; 1 || nx &gt; n || ny &amp;#x3C; 1 || ny &gt; m) continue;
                    if(in[nx][ny] == &apos;.&apos;) add(get(i, j), get(nx, ny), 1);
                    else if(in[nx][ny] != &apos;#&apos;) {
                        add(get(i, j), get(nx, ny), 1);
                    }
                }
            } else {//对于传送门点，将第二层的传送门点向第一层相邻所有能走到的点连边
                for(int k = 0; k &amp;#x3C; 4; k++) {
                    int nx = i + dir[k][0], ny = j + dir[k][1];
                    if(nx &amp;#x3C; 1 || nx &gt; n || ny &amp;#x3C; 1 || ny &gt; m) continue;
                    if(in[nx][ny] != &apos;#&apos;) {
                        add(get(i, j) + n * m, get(nx, ny), 1);
                    }
                }
            }
        }
    }
    Dijkstra();
    if(dis[get(n, m)] &gt;= 0x3f3f3f3f &amp;#x26;&amp;#x26; dis[get(n, m) + n * m] &gt;= 0x3f3f3f3f)//无法到达
        cout &amp;#x3C;&amp;#x3C; &quot;Game Over.&quot; &amp;#x3C;&amp;#x3C; endl;
    else cout &amp;#x3C;&amp;#x3C; min(dis[get(n, m)], dis[get(n, m) + n * m]) &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P3509】[POI2010]ZAB-Frog</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3509</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3509</guid><description>算法题解</description><pubDate>Wed, 07 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;数轴上有 $n$ 个点，坐标分别为 $p_1, p_2, \cdots, p_n$ 在这些点上按照某些规则跳。&lt;/p&gt;
&lt;p&gt;规则是：每次向距当前点第 $k$ 小的点跳，如果有相同距离则向下标较小的跳；&lt;/p&gt;
&lt;p&gt;求从每个点出发跳了 $m$ 次后在哪里.&lt;/p&gt;
&lt;p&gt;$1\leq k &amp;#x3C; n\leq 10^6, 1\leq m\leq 10^{18}, 1\leq p_i\leq 10^{18}$.&lt;/p&gt;
&lt;h2&gt;$\rm{Part}$ $\rm{1}$&lt;/h2&gt;
&lt;p&gt;首先，注意到 $k$ 是固定的，所以可以先预处理，对于每个点 $i$，跳一次之后的位置$\operatorname{next}_i$.&lt;/p&gt;
&lt;p&gt;这部分使用单调队列处理。&lt;/p&gt;
&lt;p&gt;我们知道，对于每个点，距离其前 $k$ 小的点分布在其两侧，可以用一段完整区间覆盖。&lt;/p&gt;
&lt;p&gt;所以我们想到，当单调队列枚举到 $i$ 时，单调队列中维护的区间，就是覆盖距离 $i$ 前 $k$ 小的点的区间。这样找到区间内距离最大的点就是我们要求的 $\operatorname{next}_i$，下面考虑考虑如何维护。&lt;/p&gt;
&lt;p&gt;对于区间 $[l, r]$ 中的一个点 $i$，距离他最远的点一定是 $l$ 和 $r$ 这两个点中的一个。如果 $r+1$ 到 $i$ 的距离小于了 $l$ 到 $i$ 的距离，说明区间应当向右滑动，如下图，$n=7, k=3$ 的例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/04/07/7f8eb682152e0.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;绿色区间是距离点 $C$ 前 $k$ 小的点，考虑距离点 $D$ 前 $k$ 小的点。&lt;/p&gt;
&lt;p&gt;$\rm{Dis(D, F)&amp;#x3C;Dis(D, A)}$，所以区间要右移一个单位，变成：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/04/07/c2fab34dc4c38.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;又因为 $\rm{Dis(D, E)&amp;#x3C;Dis(D, B)}$，所以区间要再右移一个单位，变成：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/04/07/e66fbeeb3b4ce.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;至此，我们能够求出每个点，距离其第 $k$ 小的点的下标。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;head = 1, tail = k + 1;
for(int i = 1; i &amp;#x3C;= n; i++) {
	while(tail + 1 &amp;#x3C;= n &amp;#x26;&amp;#x26; x[tail + 1] - x[i] &amp;#x3C; x[i] - x[head]) head++, tail++;
	if(x[tail] - x[i] &gt; x[i] - x[head]) nxt[i] = tail;
	else nxt[i] = head;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;$\rm{Part}$ $\rm{2}$&lt;/h2&gt;
&lt;p&gt;题目要求跳 $m$ 次之后的答案，我们使用类似快速幂的方法，倍增处理即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++) pos[i] = i;
while(m) {
	if(m &amp;#x26; 1) {
		for(int i = 1; i &amp;#x3C;= n; i++) pos[i] = next[pos[i]];
	}
	m &gt;&gt;= 1;
	for(int i = 1; i &amp;#x3C;= n; i++) next2[i] = next[i];
	for(int i = 1; i &amp;#x3C;= n; i++) next[i] = next2[next2[i]];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;最终代码在上面两部分的基础上加上 $\rm{I/O}$ 即可。&lt;/p&gt;</content:encoded></item><item><title>【CodeForces-626E】Simple Skewness</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf626e</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf626e</guid><description>算法题解</description><pubDate>Tue, 06 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $n$ 个非负整数，选一个子集，使得子集的平均值减去中位数最大。&lt;/p&gt;
&lt;p&gt;求这个最大值。&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $1-\mathcal{O}(n^2)$&lt;/h2&gt;
&lt;p&gt;因为需要考虑中位数，容易想到按照子集大小的奇偶性分类讨论。&lt;/p&gt;
&lt;h4&gt;子集大小为奇数&lt;/h4&gt;
&lt;p&gt;先来考虑奇数的情况。&lt;/p&gt;
&lt;p&gt;容易想到 $\mathcal{O}(n)$ 枚举中位数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.tonyyin.top/2021/04/05/9b79d51da9e7a.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;比如上图中，$(4, 10)$，$(3, 9)$，$(2, 8)$ 两两为一组，每次分别被取出。&lt;/p&gt;
&lt;p&gt;于是对于每个中位数，只需要 $\mathcal{O}(n)$ &lt;strong&gt;枚举两侧添加点的个数&lt;/strong&gt;，就可以确定出选取的情况，求出平均值与中位数的差。&lt;/p&gt;
&lt;p&gt;由于方案已经确定，直接输出即可。&lt;/p&gt;
&lt;p&gt;这样可以得到整体复杂度为 $\mathcal{O}(n^2)$ 的算法，期望得分未知。&lt;/p&gt;
&lt;h4&gt;子集大小为偶数&lt;/h4&gt;
&lt;h5&gt;结论&lt;/h5&gt;
&lt;p&gt;对于大小为偶数的子集，观察可以得到结论：&lt;strong&gt;对于任意大小为偶数的子集，一定存在大小为奇数的子集比其更优&lt;/strong&gt;，下面进行证明。&lt;/p&gt;
&lt;h5&gt;分析&lt;/h5&gt;
&lt;p&gt;按照上面的贪心策略任取一个大小为奇数的子集，将其升序排序，设为 ${a_1, a_2, \cdots a_{2n-1}}$.&lt;/p&gt;
&lt;p&gt;现在向子集中添加一个数，使其大小变为偶数。根据上面提到的&lt;strong&gt;贪心策略&lt;/strong&gt;，新加入的数 $x$ 有以下两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$x$ 在 $a_1$ 左侧，即 $x\leq a_1$，&lt;/li&gt;
&lt;li&gt;紧邻中位数右侧，即 $a_n\leq x\leq a_{n+1}$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在上面的图里，可以形象地看到，$x$ 的第一种情况是选择 $1$，第二种情况是在 $6, 7$ 中选一个。&lt;/p&gt;
&lt;p&gt;第一种情况，显然不会使答案变大，因为 $x$ 对平均值的影响太大了。&lt;/p&gt;
&lt;p&gt;对于第二种情况，下面进行详细证明。&lt;/p&gt;
&lt;h5&gt;证明&lt;/h5&gt;
&lt;p&gt;为了方便，我们把加入 $x$ 后的数列重新升序排序，不妨设为 ${b_1, b_2, \cdots b_{2n}}$.&lt;/p&gt;
&lt;p&gt;加入 $x$ 前的答案为：
$$
A=\frac{\sum_{i=1}^{2n}b_i-b_{n+1}}{2n-1}-b_{n}
$$
加入后的答案为：
$$
B=\frac{\sum_{i=1}^{2n}b_i}{2n}-\frac{b_{n} + b_{n + 1}}{2}
$$&lt;/p&gt;
&lt;p&gt;两者作差并化简后得到：
$$
A-B=\frac{\sum_{i=1}^{2n}b_i}{2n(2n-1)}+\frac{b_{n+1}-b_n}{2}-\frac{b_{n+1}}{2n-1}
$$
又因为 $B\geq 0$，所以：
$$
\frac{\sum_{i=1}^{2n}b_i}{2n}\geq \frac{b_n+b_{n+1}}{2}
$$
即：
$$
\frac{\sum_{i=1}^{2n}b_i}{2n(2n-1)}\geq \frac{b_n+b_{n+1}}{2(2n-1)}
$$
代入 $A-B$ 中得到：
$$
\begin{aligned}
A-B&amp;#x26;\geq \frac{b_n+b_{n+1}}{2(2n-1)}+\frac{b_{n+1}-b_n}{2}-\frac{b_{n+1}}{2n-1}\
&amp;#x26;=\frac{b_n+b_{n+1}+(2n-1)(b_{n+1}-b_n)-2b_{n+1}}{2(2n-1)}\
&amp;#x26;=\frac{b_n+b_{n+1}+2n\cdot b_{n+1}-2n\cdot b_n-b_{n+1}+b_n-2b_{n+1}}{2(2n-1)}\
&amp;#x26;=\frac{(n-1)(b_{n+1}-b_n)}{(2n-1)}\geq 0
\end{aligned}
$$
于是我们证明了：加入 $x$ 前的答案一定不小于加入 $x$ 后的答案，也就是：&lt;strong&gt;对于任意大小为偶数的子集，一定存在大小为奇数的子集比其更优&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;$\mathcal{O}(n^2)$ 代码&lt;/h4&gt;
&lt;p&gt;以下代码输出的是：&lt;strong&gt;平均值与中位数的差&lt;/strong&gt;的最大值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 2e5 + 10;
const int inf = 0x3f3f3f3f3f3f3f3f;
inline int read() {}//快读
int n, a[MAXN], sum[MAXN];
signed main() {
	n = read();
	for(int i = 1; i &amp;#x3C;= n; i++) a[i] = read();
	sort(a + 1, a + n + 1);
	for(int i = 1; i &amp;#x3C;= n; i++) sum[i] = sum[i - 1] + a[i];
	double ans = (-1.0) * inf;
	for(int i = 1; i &amp;#x3C;= n; i++) {//枚举中位数，两侧同时添加点，尽可能靠右
        double mmax = (double)(a[i]);//只取中位数
        for(int j = 1; j &amp;#x3C;= min(i - 1, n - i); j++) {//subtask1 暴力枚举两侧加2*j个点
            int add_l = sum[i - 1] - sum[i - j - 1];
            int add_r = sum[n] - sum[n - j];
            mmax = max(mmax, (double)(add_l + add_r + a[i]) / (double)(2 * j + 1));
        }
        ans = max(ans, mmax - (double)(a[i]));
	}
	printf(&quot;%.2lf\n&quot;, ans);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;$\rm{Subtask}$ $2-\mathcal{O}(n\log n)$&lt;/h2&gt;
&lt;p&gt;考虑如何把时间复杂度优化到 $\mathcal{O}(n\log n)$ 级别。&lt;/p&gt;
&lt;p&gt;$\mathcal{O}(n^2)$ 的做法中，我们先枚举中位数 $mid$，再枚举两侧添加的元素个数 $len$.&lt;/p&gt;
&lt;p&gt;在此基础上，设第 $i$ 次添加的两个元素&lt;strong&gt;下标&lt;/strong&gt;分别为 $x_i$ 和 $y_i$.&lt;/p&gt;
&lt;p&gt;当确定中位数时，答案只与平均值有关，考虑计算添加 $(x_i, y_i)$ 对平均值的贡献。&lt;/p&gt;
&lt;p&gt;贡献，也就是添加这两个数之后，&lt;strong&gt;平均值的变化量&lt;/strong&gt;，若平均值变大，则贡献为正，反之为负。&lt;/p&gt;
&lt;p&gt;显然地，&lt;strong&gt;随着 $i$ 的增长&lt;/strong&gt;，$a_{x_i}+a_{y_i}$ 会持续下降，&lt;strong&gt;对平均值的贡献也会越来越少&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们最后一次选取的两个元素 $x_{len}, y_{len}$，必定满足贡献大于 $0$，并且是最接近 $0$ 的。&lt;/p&gt;
&lt;p&gt;因此我们可以通过&lt;strong&gt;二分&lt;/strong&gt;，找到贡献紧邻最接近0的数。&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;/p&gt;
&lt;p&gt;这样就得到了 $\mathcal{O}(n\log n)$ 的算法，期望得分 $\rm{100pts}$.&lt;/p&gt;
&lt;h2&gt;$\rm{100pts}$ 代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 2e5 + 10;
const int inf = 0x3f3f3f3f3f3f3f3f;
inline int read() {
	int ret = 0; char ch = getchar();
	while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) ch = getchar();
	while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) {
		ret = ret * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	return ret;
}
int n;
int a[MAXN], sum[MAXN];
signed main() {
	n = read();
	for(int i = 1; i &amp;#x3C;= n; i++) a[i] = read();
	sort(a + 1, a + n + 1);
	for(int i = 1; i &amp;#x3C;= n; i++) sum[i] = sum[i - 1] + a[i];
	double ans = (-1.0) * inf; int ans_cnt = 0, ans_mid = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {//枚举中位数，两侧同时添加点，尽可能靠右
		int l = 1, r = min(i - 1, n - i), mid;
		double mmax = (double)(a[i]); int cnt = 0;
		while(l &amp;#x3C;= r) {
			mid = (l + r) &gt;&gt; 1;
			int sum1 = sum[i - 1] - sum[i - mid] + sum[n] - sum[n - mid + 1];
            //j = mid - 1，即加入前的平均值
			int sum2 = sum[i - 1] - sum[i - mid - 1] + sum[n] - sum[n - mid];
            //j = mid，加入两个元素后的平均值
			if((double)(sum1 + a[i]) / (double)(2 * mid - 1) &amp;#x3C; (double)(sum2 + a[i]) / (double)(2 * mid + 1)) {
				if((double)(sum2 + a[i]) / (double)(2 * mid + 1) &gt; mmax) {
					mmax = (double)(sum2 + a[i]) / (double)(2 * mid + 1);
					cnt = mid;
				}
				l = mid + 1;
			} else {
				r = mid - 1;
			}
		}
		if(mmax - double(a[i]) &gt; ans) {
			ans = mmax - (double)(a[i]);
			ans_cnt = cnt;
			ans_mid = i;
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans_cnt * 2 + 1 &amp;#x3C;&amp;#x3C; endl;
	for(int i = ans_mid - ans_cnt; i &amp;#x3C;= ans_mid; i++) cout &amp;#x3C;&amp;#x3C; a[i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
	for(int i = n - ans_cnt + 1; i &amp;#x3C;= n; i++) cout &amp;#x3C;&amp;#x3C; a[i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
	return 0;
}```
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P5677】配对统计</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5677</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5677</guid><description>算法题解</description><pubDate>Thu, 25 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个数列 $a_1, a_2, \cdots, a_n$.&lt;/p&gt;
&lt;p&gt;定义在&lt;strong&gt;有序&lt;/strong&gt;数对 $(x, y)$ 上的“&lt;strong&gt;好对&lt;/strong&gt;”：对于 $a_x$，$y\in[1, x)\cup(x, n]$ 能使 $|a_x-a_y|$ 取最小值。&lt;/p&gt;
&lt;p&gt;给定 $q\leq 3\times 10^5$ 组询问，每次询问一个区间 $[l, r]$ 中，有多少个好对。&lt;/p&gt;
&lt;p&gt;题目求：每次询问的答案 $Ans_i$ 与询问编号 $i$ 的乘积的和，即：
$$
\sum_{i=1}^{m}Ans_i\times i
$$&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;h4&gt;关于匹配&lt;/h4&gt;
&lt;p&gt;任何一个好对 $(x, y)$，都满足 $|a_x-a_y|\leq |a_x-a_i|(i\neq x)$.&lt;/p&gt;
&lt;p&gt;并且通过数据范围可以发现：$\forall i\neq j$ 都有 $a_i\neq a_j$，所以当 $x$ 固定时，确定 $y$ 的方法是非常自然的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将 $a$ 数组排序&lt;/strong&gt;，排序后的数组为 $b$，不妨设 $a_x$ 在 $b$ 中的下标为 $t$，那么 &lt;strong&gt;$a_y$ 在 $b$ 中的下标只可能为 $t-1$ 和 $t+1$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体地：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $|b_t-b_{t-1}|&amp;#x3C;|b_{t+1}-b_{t}|$，也就是 $b_{t-1}$ 距离 $b_t$ 更近，那么 $a_y$ 在 $b$ 中的下标为 $t-1$；&lt;/li&gt;
&lt;li&gt;如果 $|b_t-b_{t-1}|&gt;|b_{t+1}-b_{t}|$，此时 $b_{t+1}$ 距离 $b_t$ 最近，$a_y$ 在 $b$ 中的下标为 $t+1$；&lt;/li&gt;
&lt;li&gt;如果 $|b_t-b_{t-1}|=|b_{t+1}-b_{t}|$，$a_y$ 在 $b$ 中的下标为 $t-1$ 或 $t+1$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，我们可以非常容易地在 $O(n\log n)$ 复杂度内，&lt;strong&gt;对于每个 $x$，找到能与其构成好的配对的 $y$.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;关于询问&lt;/h4&gt;
&lt;h5&gt;思路&lt;/h5&gt;
&lt;p&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;h5&gt;方法&lt;/h5&gt;
&lt;p&gt;从左到右枚举 $[1, n]$，假设枚举到了 $i$.&lt;/p&gt;
&lt;p&gt;对于 $i$，我们已经计算过与其匹配的数，在 $a$ 中的下标 $y$，如果 $y&gt;i$，那么&lt;strong&gt;所有左端点 $l\leq i$ 且右端点 $r\geq y$ 的询问都能包含这个好对。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再考虑答案的形式 $\sum_{i=1}^{m}Ans_i\times i$，我们现在要求：左端点 $l\leq i$ 且右端点 $r\geq y$ 的询问的编号和，使用树状数组维护。&lt;/p&gt;
&lt;p&gt;于是考虑将询问按照左端点排序，在枚举到 $i$ 的时候，把所有左端点 $l\leq i$ 的询问加入到树状数组中，&lt;strong&gt;把树状数组的位置 $r$ 的权值加上这个询问的编号 $num$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样，$(i, y)$ 的贡献就等价于：树状数组中 $[y, n]$ 的区间和。&lt;/p&gt;
&lt;p&gt;如果与 $i$ 匹配的 $y&amp;#x3C;i$，在上面的方法中并没有包含到。所以我们再反向枚举一遍 $i$，方法相同。将右端点 $r\geq i$ 的询问加入树状数组，每次的贡献为树状数组中 $[1, y]$ 的区间和。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 3e5 + 10;
const int inf = 0x3f3f3f3f3f3f3f3f;
int c[MAXN];
int n, m;
int lowbit(int x) {
	return x &amp;#x26; (-x);
}
void update(int x, int k) {
	for(int i = x; i &amp;#x3C;= n; i += lowbit(i)) {
		c[i] += k;
	}
}
int query(int x) {
	int ret = 0;
	for(int i = x; i &gt; 0; i -= lowbit(i)) {
		ret += c[i];
	}
	return ret;
}
int query2(int x) {
	return query(n) - query(x - 1);
}
int a[MAXN];
struct Node{int num, val;} b[MAXN];
bool cmp(Node A, Node B) {
	return A.val &amp;#x3C;= B.val;
}
struct Query{int val, l, r;} q[MAXN];
bool cmp2(Query A, Query B) {
	return A.r &gt;= B.r;
}
bool cmp3(Query A, Query B) {
	return A.l &amp;#x3C;= B.l;
}
int rk[MAXN];
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;m);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		scanf(&quot;%lld&quot;, &amp;#x26;a[i]);
		b[i].val = a[i]; b[i].num = i;
	}
	sort(b + 1, b + n + 1, cmp);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		rk[b[i].num] = i;
	}
	for(int i = 1; i &amp;#x3C;= m; i++)  {
		q[i].val = i;
		scanf(&quot;%lld%lld&quot;, &amp;#x26;q[i].l, &amp;#x26;q[i].r);
	}
	sort(q + 1, q + m + 1, cmp2);
	int ans = 0;
	b[0].val = -inf; b[n + 1].val = inf;
	for(int i = n, j = 1; i &gt;= 1; i--) {
		while(q[j].r &gt;= i &amp;#x26;&amp;#x26; j &amp;#x3C;= m) {
			update(q[j].l, q[j].val);
			j++;
		}
        //下面六行：判断 t-1 和 t+1，哪个距离 t 更近，能与 t 构成好的匹配。
		if(b[rk[i]].val - b[rk[i] - 1].val &amp;#x3C;= b[rk[i] + 1].val - b[rk[i]].val) {
			if(b[rk[i] - 1].num &amp;#x3C; i) ans += query(b[rk[i] - 1].num);
		}
		if(b[rk[i]].val - b[rk[i] - 1].val &gt;= b[rk[i] + 1].val - b[rk[i]].val) {
			if(b[rk[i] + 1].num &amp;#x3C; i) ans += query(b[rk[i] + 1].num);
		}
	}
	sort(q + 1, q + m + 1, cmp3);
	memset(c, 0, sizeof(c));
	for(int i = 1, j = 1; i &amp;#x3C;= n; i++) {
		while(q[j].l &amp;#x3C;= i &amp;#x26;&amp;#x26; j &amp;#x3C;= m) {
			update(q[j].r, q[j].val);
			j++;
		}
		if(b[rk[i]].val - b[rk[i] - 1].val &amp;#x3C;= b[rk[i] + 1].val - b[rk[i]].val) {
			if(b[rk[i] - 1].num &gt; i) ans += query2(b[rk[i] - 1].num);
		}
		if(b[rk[i]].val - b[rk[i] - 1].val &gt;= b[rk[i] + 1].val - b[rk[i]].val) {
			if(b[rk[i] + 1].num &gt; i) ans += query2(b[rk[i] + 1].num);
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces–1500A】Going Home</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1500a</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1500a</guid><description>算法题解</description><pubDate>Fri, 19 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $n$ 个正整数 $a_1, a_2\cdots, a_n$，求出任意一组 $x\neq y\neq z\neq w$ 使得 $a_x+a_y=a_z+a_w$.&lt;/p&gt;
&lt;p&gt;$n\leq 2\times 10^5, 1\leq a_i\leq 2.5\times 10^6$.&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;首先考虑暴力，我们开一个 $2a_i\leq 5\times 10^6$ 大小的桶，记录已经找到的和，然后 $\mathcal{O}(n^2)$ 地暴力枚举，直到桶中有元素出现过 $\geq1$ 次，并且满足 $x, y, z, w$ 互不相同即可。&lt;/p&gt;
&lt;p&gt;又考虑到这只是 &lt;code&gt;div2.&lt;/code&gt; 的 &lt;code&gt;C&lt;/code&gt; 题，在这个数据范围上的 $\mathcal{O}(n\log n)$ 做法经过尝试后都行不通。&lt;/p&gt;
&lt;p&gt;赛后，我们才知道：非常不幸，这题是结论题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论：上面的暴力做法的时间复杂度为：$\mathcal{O}(\min(n^2, n+C))$，$C$ 为 $a_i+a_j$ 的值域。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;证明&lt;/h2&gt;
&lt;p&gt;首先，如果我们找到了四组&lt;strong&gt;两两不完全相同&lt;/strong&gt;的 $(x_i, y_i)$，使得：
$$
a_{x_1}+a_{y_1}=a_{x_2}+a_{y_2}=a_{x_3}+a_{y_3}=a_{x_4}+a_{y_4}
$$
那么就可以找到满足条件的 $x,y, z, w$ 了。&lt;/p&gt;
&lt;p&gt;这个结论是非常自然的，简单分类讨论就可以证明。对四个数对中，有多少个 $a_x$ 相同进行讨论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果有四个数对的 $a_{x_i}$ 相同，那么 $a_{y_1}=a_{y_2}=a_{y_3}=a_{y_4}$，所以 $y_1, y_2, y_3, y_4$ 是可行的解。&lt;/li&gt;
&lt;li&gt;如果有三个数对的 $a_{x_i}$ 相同，不妨设 $a_{x_4}$ 与另外三个数不同，那么 $(x_4, y_4), (x_1, y_1)$ 就是一组可行解。&lt;/li&gt;
&lt;li&gt;对于其他情况，显然能找到满足题意的 $x, y, z, w$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，我们只需要用 $\mathcal{O}(n^2)$ 的暴力做法，&lt;strong&gt;最差情况是：所输出的答案的 $sum$ 值出现四次。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有 $sum$ 的可能值只有 $5\times 10^6$ 种，根据抽屉原理，程序复杂度不超过 $2\times 10^7$，可以通过。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int MAXN = 2e5 + 10, MAXC = 2e6 + 5e5 + 10;
int n;
int x[MAXC &amp;#x3C;&amp;#x3C; 1], y[MAXC &amp;#x3C;&amp;#x3C; 1];
int a[MAXN], cnt[MAXC &amp;#x3C;&amp;#x3C; 1];
bool have_ans;
int main() {
	scanf(&quot;%d&quot;, &amp;#x26;n);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		scanf(&quot;%d&quot;, &amp;#x26;a[i]);
	}
	for(int i = 1; i &amp;#x3C;= n; i++) {
		for(int j = i + 1; j &amp;#x3C;= n; j++) {
			if(cnt[a[i] + a[j]]) {
				if(x[a[i] + a[j]] != i &amp;#x26;&amp;#x26; y[a[i] + a[j]] != i 
                &amp;#x26;&amp;#x26; x[a[i] + a[j]] != j &amp;#x26;&amp;#x26; y[a[i] + a[j]] != j) {
					cout &amp;#x3C;&amp;#x3C; &quot;YES&quot; &amp;#x3C;&amp;#x3C; endl &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; j &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; x[a[i] + a[j]] &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; y[a[i] + a[j]] &amp;#x3C;&amp;#x3C; endl;
					return 0;
				}
			} else {
				cnt[a[i] + a[j]]++;
				x[a[i] + a[j]] = i; y[a[i] + a[j]] = j;
			}
		}
	}
	cout &amp;#x3C;&amp;#x3C; &quot;NO&quot; &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【CodeForces-1495A】Diamond Miner</title><link>https://www.tonyyin0418.com/blog/oi-solution/cf1495a</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/cf1495a</guid><description>算法题解</description><pubDate>Fri, 12 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 $2n$ 个点，其中 $n$ 个在 $x$ 轴上，另外 $n$ 个在 $y$ 轴上。现在要求每一个 $x$ 轴上的点与一个 $y$ 轴上的点连线，每个点&lt;strong&gt;恰好被连线一次&lt;/strong&gt;。求所有线段的&lt;strong&gt;欧几里得距离之和&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/03/12/60fd2edafc780.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;首先根据数据范围 $n\leq 10^5$，在考场可以判断出贪心或二分。&lt;/p&gt;
&lt;p&gt;之后看到样例解释的配图，发现可以抽象出：对于两条线段的模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/03/12/84106fb92fa8b.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;由上图可以看出，对于四个点，只有两种连线方法：不交叉或交叉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不交叉&lt;/strong&gt;的欧几里得距离和为：$|AB|+|CD|$，&lt;strong&gt;交叉&lt;/strong&gt;的欧几里得距离为 $|AC|+|BD|$.&lt;/p&gt;
&lt;p&gt;经过肉眼观测，我们可以用小学数学证明&lt;strong&gt;不交叉一定优于交叉&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;由于三角形任意两边之和大于第三边，所以 $|AB|&amp;#x3C;|AO|+|BO|$，$|CD|&amp;#x3C;|CO|+|DO|$.&lt;/p&gt;
&lt;p&gt;于是 $|AB|+|CD|&amp;#x3C;|AO|+|BO|+|CO|+|DO|=|AC|+|BD|$.&lt;/p&gt;
&lt;p&gt;将结论推广到 $n$ 条线段，发现&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;p&gt;所以，把 $x$ 轴上的点和 $y$ 轴上的点分别按照横竖坐标&lt;strong&gt;升序排序&lt;/strong&gt;，排名相同的点进行配对，一定不会交叉。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;~考场代码~&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 1e5 + 10;
int T;
double a[MAXN], b[MAXN];
signed main() {
    cin &gt;&gt; T;
    while(T--) {
        int n, cnt1 = 0, cnt2 = 0; cin &gt;&gt; n;
        for(int i = 1; i &amp;#x3C;= 2 * n; i++) {
            int x, y; scanf(&quot;%lld%lld&quot;, &amp;#x26;x, &amp;#x26;y);
            if(x == 0) {
                if(y &amp;#x3C; 0) y *= -1;
                a[++cnt1] = y;
            }
            if(y == 0) {
                if(x &amp;#x3C; 0) x *= -1;
                b[++cnt2] = x;
            }
        }
        sort(a + 1, a + n + 1); sort(b + 1, b + n + 1);
        double ans = 0;
        for(int i = 1; i &amp;#x3C;= n; i++) {
            ans += sqrt(a[i] * a[i] + b[i] * b[i]);
        }
        printf(&quot;%.11lf\n&quot;, ans);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【APIO2010】特别行动队</title><link>https://www.tonyyin0418.com/blog/oi-solution/p3628</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p3628</guid><description>算法题解</description><pubDate>Fri, 05 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一个长度为 $n$ 的序列，每个元素有权值 $x_i$，现在要将序列分割为若干段，每段中元素连续。设 $s_i$ 表示前缀和，即 $s_i=\sum_{j=1}^{i}{x_j}$，那么对于每段 $[l, r]$，代价为：&lt;/p&gt;
&lt;p&gt;$$
a(s_{i}-s_{j-1})^2+b(s_i-s_{j-1})+c
$$&lt;/p&gt;
&lt;p&gt;其中 $a, b, c$ 为题目给定的常数。整个序列的代价为每段代价之和。&lt;/p&gt;
&lt;p&gt;求序列的代价&lt;strong&gt;最大值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^6, -{10}^7\leq b, c\leq 10^7, 1\leq x_i\leq 100$.&lt;/p&gt;
&lt;p&gt;注意：$-5\leq \textcolor{red}{a} \leq -1\textcolor{red}{&amp;#x3C;0}$.&lt;/p&gt;
&lt;h2&gt;DP&lt;/h2&gt;
&lt;p&gt;容易想到时间复杂度为 $\mathcal{O}(n^2)$ 的 DP 做法。&lt;/p&gt;
&lt;p&gt;令 $f_i$ 为前 $i$ 个物品，分若干段的最小代价，有状态转移方程：&lt;/p&gt;
&lt;p&gt;$$
f_i=\max_{j&amp;#x3C;i}{{f_j+a(s_i-s_j)^2+b(s_i-s_j)+c}}
$$&lt;/p&gt;
&lt;p&gt;其中 $s_i$ 与题目中定义相同，为 $x$ 的前缀和。&lt;/p&gt;
&lt;p&gt;但发现 $1\leq n\leq 10^6$，所以这样的 DP 无法通过此题，考虑如何优化。&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;发现对于 $f_i$ 来说，一定存在唯一的 $j&amp;#x3C;i$ 对 $f_i$ 进行了更新，使得 $f_i$ 取到最大值。所以不妨设 $j$ 为能&lt;strong&gt;使 $f_i$ 最大化的继承状态&lt;/strong&gt;， 那么对暴力的状态转移方程进行整理：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
f_i&amp;#x26;=f_j+a(s_i-s_j)^2+b_(s_i-s_j)+c\
f_i&amp;#x26;=f_j+a({s_i}^2+{s_j}^2-2s_is_j)+b(s_i-s_j)+c\
f_i&amp;#x26;=f_j+a\cdot {s_i}^2+a\cdot {s_j}^2-2a\cdot s_is_j+b\cdot s_i-b\cdot s_j+c
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;（这就很斜率优化&lt;/p&gt;
&lt;p&gt;考虑一次函数的斜截式 $y=kx+b$，将方程转化为这个形式。其中&lt;strong&gt;变量 $x, y$ 与 $j$ 有关，$b, k$ 与 $j$ 无关&lt;/strong&gt;，且要求 $x$ 随 $j$ 单调递增。&lt;/p&gt;
&lt;p&gt;那么：&lt;/p&gt;
&lt;p&gt;$$
\textcolor{red}{f_i-b\cdot s_i-a\cdot {s_i}^2}=\textcolor{purple}{(f_j+a\cdot {s_j}^2-b\cdot s_j+c)}-\textcolor{green}{2a\cdot s_i}\textcolor{blue}{s_j}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\left{
\begin{array}{lr}
y = f_j+a\cdot {s_j}^2-b\cdot s_j+c\
k = 2a\cdot s_i\
x = s_j\
b = f_i-b\cdot s_i-a\cdot {s_i}^2
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;观察 $b$ 这一项。要最大化 $f_i$，而 $-b\cdot s_i-a\cdot{s_i}^2$ 为常数，所以只要最大化 $b$ 即可。&lt;/p&gt;
&lt;p&gt;由于 $x, y$ 只与 $j$ 有关，不妨设 $(x_j, y_j)$ 为平面直角坐标系上的一些点。那么问题就转化为，&lt;strong&gt;如何选取 $j$，才能使得，斜率固定为 $k$ 的，且过点 $(x_j, y_j)$ 的直线的截距 $b$ 最大。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（斜率为定值 $k$，是因为 $2a\cdot s_i$ 也都是题目给定的定值。&lt;/p&gt;
&lt;p&gt;那么问题转化到了平面上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/03/05/67928a149500a.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;由上图可以形象的看出，使直线截距最大的点（最优决策点），就是将直线向下平移时&lt;strong&gt;第一个碰到的点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而且显然对于任意的斜率 $k$ 来说，&lt;strong&gt;最优决策点只有可能在上凸包上&lt;/strong&gt;，也就是上图中，绿色直线所覆盖的点。&lt;/p&gt;
&lt;p&gt;继续观察，在上图中可以看出，对于斜率为 $k$ 的直线，最优解 $j$ 就是&lt;strong&gt;第一个 $\operatorname{slope}(j, j+1)&amp;#x3C;k$ 的点&lt;/strong&gt;。（$\operatorname{slope}(a, b)$ 表示 $(x_a, y_a), (x_b, y_b)$ 两点构成直线的斜率）&lt;/p&gt;
&lt;p&gt;又因为本题中，$k$ 随 $i$ 递增而递减，与上凸包单调性相同，所以考虑使用单调队列解决。&lt;/p&gt;
&lt;h2&gt;单调队列&lt;/h2&gt;
&lt;p&gt;设单调队列（递增）为 $q$，队首为 $head$，队尾为 $tail$，当前要转移 $i$ 号点，具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当 $\operatorname{slope}(q_{head}, q_{head+1})\textcolor{red}{\geq}2a\cdot s_i$ 时，$head$++，（保证最优解）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时的 $q_{head}$ 就是&lt;strong&gt;最优转移点&lt;/strong&gt;，根据它和转移方程计算出 $f_i$，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 $\operatorname{slope}(q_{tail-1}, q_{tail})\textcolor{red}{\leq}\operatorname{slope}(q_{tail}, i)$ 时，$tail$--，（维护&lt;strong&gt;上凸包&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在队尾插入 $i$.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;注意多组数据即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define x(X) (s[X])
#define y(X) (f[X] + a * s[X] * s[X] - b * s[X] + c)
using namespace std;
const int MAXN = 1e6 + 10;
int n;
int a, b, c;
int s[MAXN], f[MAXN];
int head, tail, q[MAXN];
double slope(int i, int j) {
	return 1.0 * (y(j) - y(i)) / (x(j) - x(i));
}
signed main() {
	int T; scanf(&quot;%lld&quot;, &amp;#x26;T);
	while(T--) {
		scanf(&quot;%lld%lld%lld%lld&quot;, &amp;#x26;n, &amp;#x26;a, &amp;#x26;b, &amp;#x26;c);
		for(int i = 1; i &amp;#x3C;= n; i++) {
			int tmp; scanf(&quot;%lld&quot;, &amp;#x26;tmp);
			s[i] = s[i - 1] + tmp;
		}
		head = tail = 1;
		for(int i = 1; i &amp;#x3C;= n; i++) {
			while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &gt;= 2ll * a * s[i])
                head++;
			int j = q[head];
            f[i] = f[j] + a * (s[i]-s[j])*(s[i]-s[j]) + b*(s[i]-s[j]) + c;
			while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &amp;#x3C;= slope(q[tail], i))
                tail--;
			q[++tail] = i;
		}
		cout &amp;#x3C;&amp;#x3C; f[n] &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>四边形不等式优化</title><link>https://www.tonyyin0418.com/blog/oi-notes/dp-opt-quadrangle</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/dp-opt-quadrangle</guid><description>四边形不等式优化，有大部分结论的详细证明</description><pubDate>Fri, 26 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;四边形不等式优化&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;h3&gt;四边形不等式&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;设 $w(x, y)$ 是定义在整数集合上的二元函数。若对于定义域上的任意整数 $a\leq b\leq c\leq d$，都有 $w(a, d)+w(b, c)\geq w(a, c)+w(b, d)$ 成立，则称函数 $w$ 满足&lt;strong&gt;四边形不等式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;简单描述为：&lt;strong&gt;交叉小于包含&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;定理&lt;/h4&gt;
&lt;p&gt;四边形不等式还可以表述为：&lt;/p&gt;
&lt;p&gt;设 $w(x, y)$ 是定义在整数集合上的二元函数。若对于定义域上的任意整数 $a&amp;#x3C;b$，都有 $w(a, b+1)+w(a+1, b)\geq w(a, b) + w(a + 1, b + 1)$ 成立，则称函数 $w$ 满足四边形不等式。&lt;/p&gt;
&lt;p&gt;这算是另一种定义，可以证明与定义等价。&lt;/p&gt;
&lt;h3&gt;区间包含单调性&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;如果对于任意的 $l\leq l&apos;\leq r&apos;\leq r$，都有 $w(l&apos;, r&apos;)\leq w(l, r)$ 成立，那么称函数 $w$ 具有区间包含单调性。&lt;/p&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;p&gt;一般用于&lt;strong&gt;区间 DP&lt;/strong&gt; 问题，转移方程一般形如：
$$
dp[i, j]=\left{
\begin{array}{lr}
\min_{i\leq k&amp;#x3C;j}{{dp[i, k]+dp[k+1, j]+w[i, j]}}, &amp;#x26;i&amp;#x3C;j\
0, &amp;#x26;i=j\
\infty, &amp;#x26;i&gt;j
\end{array}
\right.
$$
转移方程的含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$dp[i, j]$ 表示从 $i$ 到 $j$ 的最小花费，题目一般需要求出 $dp[1, n]$.&lt;/li&gt;
&lt;li&gt;$dp[i, k]+dp[k+1][j]$ 体现递推关系，$k$ 在 $i, j$ 之间滑动，$k$ 存在一个&lt;strong&gt;最优值&lt;/strong&gt;，使得 $dp[i, j]$ 最大/最小。&lt;/li&gt;
&lt;li&gt;$w[i, j]$ 的性质非常重要。$w[i, j]$ 是与题目有关的费用，如果它满足&lt;strong&gt;四边形不等式&lt;/strong&gt;和&lt;strong&gt;区间包含单调性&lt;/strong&gt;，那么在 DP 的时候，就能进行四边形不等式优化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这类中的经典问题就是石子合并。&lt;/p&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;只需要简单的优化操作，就能把上面的 $\mathcal{O}(n^3)$ 变成 $\mathcal{O}(n^2)$.&lt;/p&gt;
&lt;p&gt;把循环 $i\leq k\leq j$ 改为 $s[i, j-1]\leq k\leq s[i+1, j]$，其中 $s[i, j]$ 记录从 $i$ 到 $j$ 的最优分割点。&lt;/p&gt;
&lt;p&gt;在计算 $dp[i, j]$ 的最值时，可以得到区间 $i, j$ 的最优分割点 $k$，记录到 $s[i, j]$ 中即可。&lt;/p&gt;
&lt;p&gt;也就是说优化后的状态转移方程为：
$$
dp[i, j]=\left{
\begin{array}{lr}
\min_{s[i, j-1]\leq k\leq s_[i+1, j]}{{dp[i, k]+dp[k+1, j]+w[i, j]}}, &amp;#x26;i&amp;#x3C;j\
0, &amp;#x26;i=j\
\infty, &amp;#x26;i&gt;j
\end{array}
\right.
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++) {
	dp[i][i] = 0;
	s[i][i] = i; // s[][] 的初始化
}
for(int len = 2; len &amp;#x3C;= n; len++) {
    for(int i = 1; i &amp;#x3C;= n - len + 1; i++) {
        int j = i + len - 1;
        for(int k = s[i][j - 1]; k &amp;#x3C;= s[i + 1][j]; k++) { //四边形不等式优化
            if(dp[i][j] &gt; dp[i][k] + dp[k + 1][j] + w[i][j]) {
                dp[i][j] = dp[i][k] + dp[k + 1][j] + w[i][j];
                s[i][j] = k;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;考场上&lt;/strong&gt;，对于能否使用四边形不等式优化的判断，一般情况下随机数据+$\mathcal{O}(n^3)$ DP之后，编程判断是否满足单调性就可以了，可以不用数学方法证明。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;h3&gt;引理1&lt;/h3&gt;
&lt;h4&gt;描述&lt;/h4&gt;
&lt;p&gt;对于上面那种形式的状态转移方程，如果 $w[i, j]$ 满足四边形不等式和区间包含单调性，那么 $dp[i, j]$ 也满足四边形不等式。&lt;/p&gt;
&lt;h4&gt;证明&lt;/h4&gt;
&lt;p&gt;即证，对于 $a\leq b\leq c\leq d$，有 $dp[a, c]+dp[b, d]\leq dp[a, d]+dp[b, c]$.&lt;/p&gt;
&lt;p&gt;不妨设 $dp[a, c]$ 的最优值在 $x$ 取到，$dp[b, d]$ 的最优值在 $y$ 取到，且 $x &amp;#x3C; y$，&lt;/p&gt;
&lt;p&gt;于是可以得到：
$$
\begin{aligned}
dp[a, c]+dp[b, d] &amp;#x26;= dp[a, x]+dp[x + 1, c] + w[a, c]+dp[b, y]+dp[y+1, d]+w[b, d]\
\
&amp;#x26;\leq dp[a, x]+dp[x + 1, c]+w[a, d]+dp[b, y]+dp[y + 1, d]+w[b, c]\
\
&amp;#x26;\leq dp[a, x]+dp[x + 1 , d]+w[a, d]+dp[b, y]+dp[y+1, c]+w[b, c]\
\
&amp;#x26;=dp[a, d]+dp[b, c]
\end{aligned}
$$
其中第三行用到了结论 $dp[x + 1, c]+dp[y + 1, d]\leq dp[x + 1, d]+dp[y + 1, c]$，可以同理递归证明。&lt;/p&gt;
&lt;p&gt;同理若 $y&amp;#x3C;x$ 命题也成立。&lt;/p&gt;
&lt;h3&gt;引理2&lt;/h3&gt;
&lt;h4&gt;描述&lt;/h4&gt;
&lt;p&gt;记 $s[i, j]=k$ 是 $dp[i, j]$ 取得最优值时的 $k$，如果 dp 满足四边形不等式，那么有 $s[i, j-1]\leq s[i, j]\leq s[i+1, j]$，即 $s[i, j-1]\leq k\leq s[i+1, j]$.&lt;/p&gt;
&lt;h4&gt;证明&lt;/h4&gt;
&lt;p&gt;记 $f[k, i, j]=dp[i, k]+dp[k + 1, j] + w[i, j]$，&lt;/p&gt;
&lt;p&gt;只需证所有不在 $[s[i, j-1], s[i+1, j]]$ 区间内的决策点肯定不是最优的。&lt;/p&gt;
&lt;p&gt;那么决策点位置有两种：在区间左右两侧，不妨设在左侧。&lt;/p&gt;
&lt;p&gt;也就是说，记 $x=s[i, j-1]$，决策点 $y\in(i, x)$.&lt;/p&gt;
&lt;p&gt;此时若证明证 $f[x, i, j]\leq f[y, i, j]$，就能说明 $y\in(i, x)$ 不是最优的。&lt;/p&gt;
&lt;p&gt;根据 $s$ 的定义，显然有 $f[x, i, j-1]\leq f[y, i, j-1]$，&lt;/p&gt;
&lt;p&gt;故证明 $f[y, i, j-1]-f[x, i, j-1]\leq f[y, i, j]-f[x, i, j]$ 即可，&lt;/p&gt;
&lt;p&gt;移项得到只需证 $f[y, i, j-1]+f[x, i, j]\leq f[y, i, j]+f[x, i, j-1]$，&lt;/p&gt;
&lt;p&gt;根据 $f$ 的定义，这就等价于证明：
$$
\begin{aligned}
dp[i, y]+dp[y+1, j-1]+w[i, j-1]+dp[i, x]+dp[x+1, y]+w[i, j]\
\leq dp[i, y]+dp[y + 1, j]+w[i, j]+dp[i, x]+dp[x + 1, j-1]+w[i, j-1]
\end{aligned}
$$
消去相同项，只需证：
$$
dp[y+1, j-1]+dp[x + 1, y]\leq dp[y+1, j] +dp[x + 1, j-1]
$$
又因为 $dp$ 满足四边形不等式，故上式成立。&lt;/p&gt;
&lt;p&gt;对于决策点在区间右侧的情况同理可证。&lt;/p&gt;
&lt;h3&gt;定理——Knuth-Yao DP Speedup Theorem&lt;/h3&gt;
&lt;h4&gt;描述&lt;/h4&gt;
&lt;p&gt;如果 $w(i, j)$ 满足四边形不等式和区间包含单调性，则用 DP 计算 $dp$ 数组的时间复杂度是 $O(n^2)$ 的。&lt;/p&gt;
&lt;h4&gt;证明&lt;/h4&gt;
&lt;p&gt;$$
\begin{aligned}
dp[i, j]=\left{
\begin{array}{lr}
\min_{s[i, j-1]\leq k\leq s_[i+1, j]}{{dp[i, k]+dp[k+1, j]+w[i, j]}}, &amp;#x26;i&amp;#x3C;j\
0, &amp;#x26;i=j\
\infty, &amp;#x26;i&gt;j
\end{array}
\right.
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;在求 $dp[i, j]$ 的时候，$s[i, j-1]$ 和 $s[i+1, j]$ 一定已经算出，这样缩小决策变量范围进行优化。
$$
\begin{align}
&amp;#x26;\sum_{len=2}^n\sum_{i=1}^{n-len+1}(1+s[i+1, i+len-1]-s[i, i+len-2])\
&amp;#x26;=\sum_{len=2}^{n}
\left[
\sum_{i=1}^{n-len+1}(1) + \sum_{i=1}^{n-len+1}(s[i+1, i+len-1]) - 	\sum_{i=1}^{n-len+1}(s[i, i+len-2])
\right]\
&amp;#x26;=\sum_{len=2}^{n}
\left{
(n-len+1)+s[n-len+2, n]-s[1, len-1]
\right}\
&amp;#x26;\leq \sum_{len=2}^{n}(2n-len+1)
\end{align}
$$
所以复杂度为 $\mathcal{O}(n^2)$.&lt;/p&gt;</content:encoded></item><item><title>斜率优化</title><link>https://www.tonyyin0418.com/blog/oi-notes/dp-opt-slope</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/dp-opt-slope</guid><description>斜率优化 DP 的笔记，有五道例题 + 讲解</description><pubDate>Fri, 26 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;斜率优化DP&lt;/h1&gt;
&lt;p&gt;五道题，套路基本相同，~~期望2h讲完~~.&lt;/p&gt;
&lt;h2&gt;P3195 - 玩具装箱&lt;/h2&gt;
&lt;h3&gt;题意&lt;/h3&gt;
&lt;p&gt;有 $n$ 个玩具，第 $i$ 个玩具价值为 $c_i$。要求将这 $n$ 个玩具排成一排，分成若干段。对于一段 $[l,r]$，它的代价为：
$$
(r-l+\sum_{i=l}^r c_i-L)^2
$$
求分段的最小代价。&lt;/p&gt;
&lt;p&gt;$1\le n\le 5\times 10^4,1\le L,0\le c_i\le 10^7$.&lt;/p&gt;
&lt;h3&gt;DP&lt;/h3&gt;
&lt;p&gt;令 $f_i$ 表示前 $i$ 个物品，分若干段的最小代价。&lt;/p&gt;
&lt;p&gt;状态转移方程：
$$
f_i=\min_{j&amp;#x3C;i}{{f_j+(pre_i-pre_j+i-j-1-L)^2}}
$$
其中 $pre_i$ 表示 $c$ 数列中，前 $i$ 个数的和，即 $\sum_{j=1}^i c_j$。&lt;/p&gt;
&lt;p&gt;时间复杂度为 $O(n^2)$，无法解决本题。&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;简化状态转移方程式：令 $s_i=pre_i+i,L&apos;=L+1$，则
$$
f_i=\min_{j&amp;#x3C;i}{f_j+(s_i-s_j-L&apos;)^2}
$$
我们目前优化的目的，是尽快地找到上面式子中的 $j$，也就是令 $f_i$ 最小化的继承状态。&lt;/p&gt;
&lt;p&gt;为了表示方便，设 $j$ 为能&lt;strong&gt;使 $f_i$ 最小化的继承状态&lt;/strong&gt;，所以有：
$$
f_i=f_j+(s_i-s_j-L&apos;)^2
$$
考虑一次函数的斜截式 $y=kx+b$，将方程转化为这个形式。&lt;/p&gt;
&lt;p&gt;其中&lt;strong&gt;变量 $x, y$ 与 $j$ 有关，$b, k$ 与 $j$ 无关&lt;/strong&gt;，且要求 $x$ 随 $j$ 单调递增，$b$ 中包含 $f_i$（建模需要）。&lt;/p&gt;
&lt;p&gt;按照上面的规则，对方程进行整理：
$$
\begin{aligned}
f_i&amp;#x26;=f_j+[s_i-(s_j+L&apos;)]^2\
f_i&amp;#x26;=f_j+{s_i}^2-2s_i(s_j+L&apos;)+(s_j+L&apos;)^2\
f_i-{s_i}^2+2s_i(s_j+L&apos;)&amp;#x26;=f_j+(s_j+L&apos;)^2\
\end{aligned}
$$
也就是：
$$
f_j+(s_j+L&apos;)^2=2s_i(s_j+L)+f_i-{s_i}^2
$$
与一次函数 $y=kx+b$ 对照地看，其中：
$$
\left{
\begin{array}{&lt;strong&gt;lr&lt;/strong&gt;}&lt;/p&gt;
&lt;p&gt;y = f_j+(s_j+L&apos;)^2\
k = 2s_i\
x = s_j+L\
b = f_i-{s_i}^2&lt;/p&gt;
&lt;p&gt;\end{array}
\right.
$$
我们把 $(x, y)$ 看成平面直角坐标系上的点。那么现在 $(x_j, y_j)$ 的意义就是：&lt;strong&gt;直线 $y=kx+b$ 上的一个点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;又因为我们的目的是最小化 $f_i$，在上面的表示当中，$f_i$ 只与直线的截距 $b$ 有关。所以显然地，问题转化为：&lt;strong&gt;如何选取 $j$ ，才能使得过点 $(x_j, y_j)$ 的直线的截距 $b$ 最小。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意到：&lt;strong&gt;直线的斜率不变&lt;/strong&gt;。于是相当于平移直线 $y=kx$ ，直到其经过图中的一个点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/02/25/39c3696467e33.png&quot; alt=&quot;20190701195640154&quot;&gt;&lt;/p&gt;
&lt;p&gt;如图，为了最小化 $b$，显然除了&lt;strong&gt;折线上面的点&lt;/strong&gt;，都不可能是最优解。（这条折线上的点集就是凸包的一部分。&lt;/p&gt;
&lt;p&gt;在折线上任取三个点 $A, B, C$ 满足 $x_A&amp;#x3C;x_B&amp;#x3C;x_C$，一定满足 $\operatorname{slope}(A,B)&amp;#x3C;\operatorname{slope}(B, C)$.&lt;/p&gt;
&lt;p&gt;由图像又易知，最优解 $j$ 就是&lt;strong&gt;第一个 $\operatorname{slope}(j, j+1)&gt;k$ 的点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是可以得到 $\mathcal{O}(n\log n)$ 的算法。（处理凸包后，每次利用单调性二分）&lt;/p&gt;
&lt;p&gt;由于随着 $i$ 的增长，$k=2s_i$ 也是单调递增的，不难发现可以用单调队列得到 $\mathcal{O}(n)$ 的算法。&lt;/p&gt;
&lt;p&gt;设单调队列（递增）为 $q$，队首为 $head$，队尾为 $tail$，当前要转移 $i$ 号点，那么具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{head}, q_{head+1})\leq2 s_i$ 时，$head$++，（保证最优解）&lt;/li&gt;
&lt;li&gt;此时的 $q_{head}$ 就是&lt;strong&gt;最优转移点&lt;/strong&gt;，根据它和转移方程计算出 $f_i$，&lt;/li&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{tail-1}, q_{tail})\geq\operatorname{slope}(q_{tail}, i)$ 时，$tail$--，（维护&lt;strong&gt;下凸包&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;在队尾插入 $i$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;https://www.geogebra.org/geometry/fcxpkkf4&quot;&gt;图&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXN = 5e4 + 10;
int n, L;
int c[MAXN], s[MAXN], f[MAXN];
double slope(int i, int j) {
	return ((f[j]+(s[j]+L)*(s[j]+L)) - (f[i]+(s[i]+L)*(s[i]+L))) / (double)(s[j] - s[i]);
}
int head, tail, q[MAXN];
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;L);
	L += 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		scanf(&quot;%lld&quot;, &amp;#x26;c[i]);
		s[i] = s[i - 1] + c[i] + 1;
	}
	head = tail = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &amp;#x3C;= 2 * s[i]) {
            head++;
        }
		f[i] = f[q[head]] + (s[i] - s[q[head]] - L) * (s[i] - s[q[head]] - L);
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &gt;= slope(q[tail], i)) {
            tail--;
        }
		q[++tail] = i;
	}
	printf(&quot;%lld&quot;, f[n]);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P3628 [APIO2010]特别行动队&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;你有一支由 $n$ 名预备役士兵组成的部队，士兵从 $1$ 到 $n$ 编号，你要将他们拆分成若干特别行动队调入战场。出于默契的考虑，同一支特别行动队中队员的编号&lt;strong&gt;应该连续&lt;/strong&gt;，即为形如 $(i, i + 1,\cdots, i+k)$ 的序列。所有的队员都应该&lt;strong&gt;属于且仅属于一支&lt;/strong&gt;特别行动队。&lt;/p&gt;
&lt;p&gt;编号为 $i$ 的士兵的初始战斗力为 $x_i$，一支特别行动队的初始战斗力 $X$ 为队内士兵初始战斗力之和，即 $x_i + x_{i+1} + \cdots + x_{i + k}$.&lt;/p&gt;
&lt;p&gt;通过长期的观察，你总结出对于一支初始战斗力为 $X$ 的特别行动队，其修正战斗力 $X&apos;=aX^2+bX+c$，其中 $a, b, c$ 是已知的系数 $a &amp;#x3C; 0$. 作为部队统帅，现在你要为这支部队进行编队，使得所有特别行动队的修正战斗力之和最大。&lt;/p&gt;
&lt;p&gt;试求出这个&lt;strong&gt;最大&lt;/strong&gt;和。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 10^6, -5\leq a\leq -1, -10^7\leq b, c\leq 10^7, 1\leq x_i\leq 100$.&lt;/p&gt;
&lt;h3&gt;DP&lt;/h3&gt;
&lt;p&gt;与上一题类似。&lt;/p&gt;
&lt;p&gt;令 $f_i$ 表示前 $i$ 个物品，分若干段的最小代价，有状态转移方程：
$$
f_i=\max_{j&amp;#x3C;i}{{f_j+a(s_i-s_j)^2+b(s_i-s_j)+c}}
$$
其中 $s_i$ 是 $x$ 数列的前缀和，也就是 $s_i=\sum_{j=1}^i{x_j}$.&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;设 $j$ 是能&lt;strong&gt;使 $f_i$ 最大化&lt;/strong&gt;的继承状态，那么整理状态转移方程：
$$
\begin{aligned}
f_i&amp;#x26;=f_j+a(s_i-s_j)^2+b_(s_i-s_j)+c\
f_i&amp;#x26;=f_j+a({s_i}^2+{s_j}^2-2s_is_j)+b(s_i-s_j)+c\
f_i&amp;#x26;=f_j+a\cdot {s_i}^2+a\cdot {s_j}^2-2a\cdot s_is_j+b\cdot s_i-b\cdot s_j+c
\end{aligned}
$$
考虑一次函数斜截式 $y=kx+b$ 或 $b=y-kx$，将状态转移方程变换为这个形式。&lt;/p&gt;
&lt;p&gt;其中&lt;strong&gt;变量 $x, y$ 与 $j$ 有关，$b, k$ 与 $j$ 无关&lt;/strong&gt;。
$$
\textcolor{red}{f_i-b\cdot s_i-a\cdot {s_i}^2}=\textcolor{purple}{(f_j+a\cdot {s_j}^2-b\cdot s_j+c)}-\textcolor{green}{2a\cdot s_i}\textcolor{blue}{s_j}
$$
所以：
$$
\left{
\begin{array}{lr}
y = f_j+a\cdot {s_j}^2-b\cdot s_j+c\
k = 2a\cdot s_i\
x = s_j\
b = f_i-b\cdot s_i-a\cdot {s_i}^2
\end{array}
\right.
$$
要最大化 $f_i$，也就是&lt;strong&gt;最大化截距 $b$&lt;/strong&gt;，把 $(x_j, y_j)$ 看作平面直角坐标系上的点。&lt;/p&gt;
&lt;p&gt;那么由于本题中 $k=2as_i$ ，是&lt;strong&gt;单调递减&lt;/strong&gt;的，与上凸包单调性相同。所以与上一题类似，选取的点一定只在&lt;strong&gt;上凸包&lt;/strong&gt;上，并且是&lt;strong&gt;第一个斜率小于 $k$ 的点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以用单调队列维护：&lt;/p&gt;
&lt;p&gt;设单调队列（递增）为 $q$，队首为 $head$，队尾为 $tail$，当前要转移 $i$ 号点，那么具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{head}, q_{head+1})\textcolor{red}{\geq}2a\cdot s_i$ 时，$head$++，（保证最优解）&lt;/li&gt;
&lt;li&gt;此时的 $q_{head}$ 就是&lt;strong&gt;最优转移点&lt;/strong&gt;，根据它和转移方程计算出 $f_i$，&lt;/li&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{tail-1}, q_{tail})\textcolor{red}{\leq}\operatorname{slope}(q_{tail}, i)$ 时，$tail$--，（维护&lt;strong&gt;上凸包&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;在队尾插入 $i$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define x(X) (s[X])
#define y(X) (f[X] + a * s[X] * s[X] - b * s[X] + c)
using namespace std;
const int MAXN = 1e6 + 10;
int n;
int a, b, c;
int s[MAXN], f[MAXN];
int head, tail, q[MAXN];
double slope(int i, int j) {
	return 1.0 * (y(j) - y(i)) / (x(j) - x(i));
}
signed main() {
	scanf(&quot;%lld%lld%lld%lld&quot;, &amp;#x26;n, &amp;#x26;a, &amp;#x26;b, &amp;#x26;c);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		int tmp; scanf(&quot;%lld&quot;, &amp;#x26;tmp);
		s[i] = s[i - 1] + tmp;
	}
	head = tail = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &gt;= 2ll * a * s[i]) {
            head++;
        }
		int j = q[head]; f[i] = f[j] + a * (s[i]-s[j])*(s[i]-s[j]) + b * (s[i]-s[j]) + c;
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &amp;#x3C;= slope(q[tail], i)) {
            tail--;
        }
		q[++tail] = i;
	}
	cout &amp;#x3C;&amp;#x3C; f[n] &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P3648 [APIO2014]序列分割&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;你正在玩一个关于长度为 $n$ 的非负整数序列的游戏，序列中第 $i$ 个元素的值为 $a_i$. 这个游戏中，你需要把序列分为 $k+1$ 个非空的块。为了得到 $k+1$ 块，你需要重复下面的操作 $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;每次操作后，你将获得那两个新产生的块的&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;$2\leq n\leq 10^5, 1\leq k\leq \min{{n-1, 200}}$.&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;首先可以发现，最终的&lt;strong&gt;分数与切的顺序无关&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;证明：对于长度为 $3$ 的序列 $xyz$，将其分为三部分，只有两种方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先分割 $xy$，再分割 $yz$，贡献为 $x(y+z)+yz=xy + yz + xz$，&lt;/li&gt;
&lt;li&gt;先分割 $yz$，再分割 $xy$，贡献为 $(x+y)z + xy=xy+yz+xz$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;显然可以推广到更大的长度，于是结论得证。&lt;/p&gt;
&lt;h3&gt;DP&lt;/h3&gt;
&lt;p&gt;由于上面的结论，假设所有切割是按照&lt;strong&gt;从左到右&lt;/strong&gt;的顺序进行的。&lt;/p&gt;
&lt;p&gt;定义 $\operatorname{DP}&lt;em&gt;{i, j}$ 表示前 $i$ 个数，进行了 $j$ 次切割的最大得分，那么有状态转移方程：
$$
\operatorname{DP}&lt;/em&gt;{i, k}=\max_{j&amp;#x3C;i}{{\operatorname{DP}&lt;em&gt;{j, k-1}+s_j(s_i-s_j)}}
$$
其中 $s$ 是题目序列中的元素前缀和，即 $s_i=\sum&lt;/em&gt;{j=1}^{i}a_j$.&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;考虑到斜率优化的形式，&lt;strong&gt;需要 $\operatorname{DP}$ 变成一维&lt;/strong&gt;，又发现状态转移方程中，$\operatorname{DP}&lt;em&gt;i$ 只和 $\operatorname{DP}&lt;/em&gt;{i-1}$ 有关，所以可以&lt;strong&gt;滚动数组&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设 $f_i=\operatorname{DP}&lt;em&gt;{i, k}$，$g_j=\operatorname{DP}&lt;/em&gt;{j, k-1}$，那么方程可以写为：
$$
f_i=\max_{j&amp;#x3C;i}{{g_j+s_j(s_i-s_j)}}
$$
这就很容易想到斜率优化。&lt;/p&gt;
&lt;p&gt;设 $j$ 是能&lt;strong&gt;使 $f_i$ 最大化&lt;/strong&gt;的继承状态，那么状态转移方程化为：
$$
f_i=g_j+s_j(s_i-s_j)
$$
考虑一次函数斜截式 $y=kx+b$，要把方程转化为这个形式。&lt;/p&gt;
&lt;p&gt;其中&lt;strong&gt;变量 $x, y$ 与 $j$ 相关，常量 $b, k$ 与 $j$ 无关&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是：
$$
\begin{aligned}
f_i&amp;#x26;=g_j+s_j(s_i-s_j)\
f_i&amp;#x26;=g_j+s_is_j-{s_j}^2\
g_j-{s_j}^2&amp;#x26;=-s_is_j+f_i
\end{aligned}
$$
所以：
$$
\left{
\begin{array}{&lt;strong&gt;lr&lt;/strong&gt;}
y = g_j-{s_j}^2\
k = -s_i\
x = s_j\
b = f_i
\end{array}
\right.
$$
这里斜率 $k$ 是&lt;strong&gt;单调下降&lt;/strong&gt;的，并且最优决策点 $(x_j, y_j)$ 是&lt;strong&gt;第一个满足 $\operatorname{slope}(j, j+1) &amp;#x3C; k$ 的点&lt;/strong&gt;。所以最优决策点一定在&lt;strong&gt;上凸包&lt;/strong&gt;上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define X(i) (s[i])
#define Y(i) (g[i] - s[i] * s[i])
using namespace std;
const long double inf = -1e18;
const int MAXN = 1e5 + 10;
int n, k;
int s[MAXN], f[MAXN], g[MAXN];
double slope(int i, int j) {
	if(s[j] == s[i]) return inf;
	return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
int to[210][MAXN];
signed main() {
	scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;k);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		int tmp; scanf(&quot;%lld&quot;, &amp;#x26;tmp);
		s[i] = s[i - 1] + tmp;
	}
	for(int l = 1; l &amp;#x3C;= k; l++) {
		head = tail = 1; q[1] = 0;
		for(int i = 1; i &amp;#x3C;= n; i++) {
			while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &gt;= -1 * s[i]) {
                ++head;
            }
			int j = q[head]; to[l][i] = q[head];
			f[i] = g[j] + s[j] * (s[i] - s[j]);
			while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &amp;#x3C;= slope(q[tail], i)) { 
                tail--;
            }
			q[++tail] = i;
		}
		memcpy(g, f, sizeof(f));//把f复制到g
	}
	printf(&quot;%lld\n&quot;, f[n]);
	for(int i = k, j = n; i &gt;= 1; i--) {
		j = to[i][j];
		printf(&quot;%lld &quot;, j);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P4360 [CEOI2004]锯木厂选址&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;从山顶到山底下沿着一条直线种植了 $n$ 颗老树。当地的政府决定把它们砍下来。为了不浪费任何一颗木材，树被砍倒后要运送到锯木厂。&lt;/p&gt;
&lt;p&gt;木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂，使得运输的费用总和最小。假定运输每公斤木材，移动每米需要一分钱。&lt;/p&gt;
&lt;p&gt;题目给定第 $i$ 颗树的重量 $w_i$ ，以及第 $i$ 颗树与第 $i+1$ 颗树之间的距离 $d_i$.&lt;/p&gt;
&lt;p&gt;请计算&lt;strong&gt;最小运输费用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$2\leq n\leq 20000, 1\leq w_i, d_i\leq 10000$，保证在 long long 范围内。&lt;/p&gt;
&lt;h3&gt;DP&lt;/h3&gt;
&lt;p&gt;https://blog.csdn.net/forever_dreams/article/details/97973474&lt;/p&gt;
&lt;p&gt;这题难点可能在这部分…）&lt;/p&gt;
&lt;p&gt;定义 $f_i$ 为以 $i$ 作为第二个锯木厂的最小花费，$s_i$ 为 $w_i$ 的前缀和，$dis_i$ 为 $d_i$ 的后缀和，$cost$ 为 $w_i\times dis_i$ 的总和。&lt;/p&gt;
&lt;p&gt;转移就是通过枚举第一个锯木厂，然后&lt;strong&gt;减去省掉的花费&lt;/strong&gt;。有下面的转移方程：
$$
f_i=\min_{j&amp;#x3C;i}{{cost - s_j\times dis_j-(s_i-s_j)\times dis_i}}
$$&lt;/p&gt;
&lt;h3&gt;斜率优化&lt;/h3&gt;
&lt;p&gt;这部分和前面都一样，所以字越来越少……&lt;/p&gt;
&lt;p&gt;设 $j$ 最优，化简：
$$
\begin{aligned}
f_i&amp;#x26;=cost-s_j\times dis_j-s_i\times dis_i+s_j\times dis_i\
s_j\times dis_j&amp;#x26;=s_j\times dis_i+(cost-f_i-s_i\times dis_i)
\end{aligned}
$$
所以：
$$
\left{
\begin{array}{&lt;strong&gt;lr&lt;/strong&gt;}
y = s_j\times dis_j\
k = dis_i\
x = s_j\
b = cost-f_i-s_i\times dis_i
\end{array}
\right.
$$
$k=dis_i$ 是后缀和，单调递减。又因为要最小化 $f_i$，所以要最大化 $b$。&lt;/p&gt;
&lt;p&gt;所以最优决策点 $(x_j, y_j)$ 是第一个满足 $\operatorname{slope}(j, j+1)&amp;#x3C;k$ 的点，单调队列维护上凸包即可。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define X(i) (s[i])
#define Y(i) (s[i] * dis[i])
using namespace std;
const int MAXN = 2e4 + 10;
int n;
int w[MAXN], d[MAXN];
int cost, s[MAXN], dis[MAXN], f[MAXN];
double slope(int i, int j) {
	return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
int ans = 1e18;
signed main() {
	scanf(&quot;%lld&quot;, &amp;#x26;n);
	for(int i = 1; i &amp;#x3C;= n; i++) scanf(&quot;%lld%lld&quot;, &amp;#x26;w[i], &amp;#x26;d[i]);
	for(int i = 1; i &amp;#x3C;= n; i++) s[i] = s[i - 1] + w[i];
	for(int i = n; i &gt;= 1; i--) dis[i] = dis[i + 1] + d[i];
	for(int i = 1; i &amp;#x3C;= n; i++) cost += w[i] * dis[i];
	head = tail = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &gt;= dis[i]) ++head;
		int j = q[head];
		f[i] = cost - s[j] * dis[j] - (s[i] - s[j]) * dis[i];
		ans = min(ans, f[i]);
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &amp;#x3C;= slope(q[tail], i)) tail--;
		q[++tail] = i;
	}
	printf(&quot;%lld\n&quot;, ans);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P2900 [USACO08MAR]Land Acquisition G&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;翻译的不是很好，所以我简化一下题面。&lt;/p&gt;
&lt;p&gt;给定 $n$ 个矩阵的宽 $w$ 和高 $h$，每次选一个矩阵&lt;strong&gt;集合&lt;/strong&gt;，代价为集合中的 $\max{{w}}\times \max{{h}}$。&lt;/p&gt;
&lt;p&gt;任意两次之间没有重复的矩阵，求取完所有矩阵的&lt;strong&gt;最小代价&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;注意到对于两个矩阵 $i, j$，如果 $w_i\leq w_j$ 并且 $h_i\leq h_j$，那么 $i$ 和 $j$ 一定可以放到一组，$i$ &lt;strong&gt;对答案没有贡献&lt;/strong&gt;，不用考虑，可以直接删去。&lt;/p&gt;
&lt;p&gt;比如下图当中的 $1$ 和 $2$. 矩阵 $2$ 能完全被 $1$ 包含，所以 $2$ 没有贡献，可以删去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/02/25/53d870c086029.png&quot; alt=&quot;P2900-1&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是考虑先删除所有 $i$ 这样的矩阵。&lt;/p&gt;
&lt;p&gt;具体做法就是：按照 $h$ 为第一关键字，$w$ 为第二关键字&lt;strong&gt;从大到小&lt;/strong&gt;排序，之后&lt;strong&gt;双指针&lt;/strong&gt;扫一遍即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//a已经降序排序
int t = 0;
for(int i = 1; i &amp;#x3C;= n; i++) {
	if(a[i].w &gt; a[t].w) a[++t] = a[i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上图，剩下的只有 $1, 3$ 号矩阵。&lt;/p&gt;
&lt;p&gt;删完之后剩下的就是一个，$h$ 严格&lt;strong&gt;单调递减&lt;/strong&gt;，$w$ 严格&lt;strong&gt;单调递增&lt;/strong&gt;的序列。考虑 $DP$ 解决。&lt;/p&gt;
&lt;h3&gt;DP&lt;/h3&gt;
&lt;p&gt;设 $f_i$ 为新序列中，取走前 $i$ 个矩阵的&lt;strong&gt;最小&lt;/strong&gt;花费，$w$ 为宽（单调递增），$h$ 为高（单调递减），那么有转移方程：
$$
f_i=\min_{j=0}^{i}{{f_{j}+w_ih_{j+1}}}
$$
相当于，把 $j+1$ 到 $i$ 这一段合为一组取走。由于单调性，所以这一组的 $\max{{w}}=w_i$，$\max{{h}}=h_{j+1}$.&lt;/p&gt;
&lt;p&gt;复杂度 $\mathcal{O}(n^2)$。这显然可以斜率优化。&lt;/p&gt;
&lt;h3&gt;斜率优化&lt;/h3&gt;
&lt;p&gt;我们目前优化的目的，是尽快地找到 $f_i$ 到底是由哪个 $j$ 转移过来的，也就是尽快找到最小化 $f_i$ 的继承状态。&lt;/p&gt;
&lt;p&gt;为了表示方便，设 $j$ 为能&lt;strong&gt;使 $f_i$ 最小化的继承状态&lt;/strong&gt;，所以有：
$$
f_i=f_{j}+w_ih_{j+1}
$$
考虑一次函数斜截式 $y=kx+b$ 或 $b=y-kx$，将状态转移方程变换为这个形式：
$$
f_{j}=-w_ih_{j+1}+f_i
$$
其中&lt;strong&gt;变量 $x, y$ 与 $j$ 有关，$b, k$ 与 $j$ 无关&lt;/strong&gt;。且要求 $x$ 随 $j$ 单调递增，$b$ 中包含 $f_i$（斜率优化基本操作）。&lt;/p&gt;
&lt;p&gt;所以可以构造出直线方程为：
$$
\left{
\begin{array}{&lt;strong&gt;lr&lt;/strong&gt;}
y = f_j\
k = w_i\
x = -h_{j+1}\
b = f_i
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;$x$ 随 $j$ 单调递增，$k$ 随 $i$ 单调递增，要使截距 $b=f_i$ 最小，最优解 $j$ 就是从左往右枚举到的&lt;strong&gt;第一个 $\operatorname{slope}(j, j+1)&gt;k$ 的点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以单调队列维护下凸包即可。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define X(i) (-a[i+1].h)
#define Y(i) (f[i])
#define int long long
using namespace std;
const int MAXN = 5e4 + 10;
int n;
struct Node{
	int w, h;
} a[MAXN];
int f[MAXN];
bool cmp(Node i, Node j) {
	return (i.h &gt; j.h) || (i.h == j.h &amp;#x26;&amp;#x26; i.w &gt; j.w);
}
double slope(int i, int j) {
	return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
signed main() {
	scanf(&quot;%lld&quot;, &amp;#x26;n);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		scanf(&quot;%lld%lld&quot;, &amp;#x26;a[i].w, &amp;#x26;a[i].h);
	}
	sort(a + 1, a + n + 1, cmp);
	int t = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		if(a[i].w &gt; a[t].w) a[++t] = a[i];
	}
	n = t;
	head = tail = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &amp;#x3C;= a[i].w) {
			head++;
		}
		int j = q[head]; f[i] = f[j] + a[i].w * a[j + 1].h;
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &gt;= slope(q[tail], i)) {
			tail--;
		}
		q[++tail] = i;
	}
	cout &amp;#x3C;&amp;#x3C; f[n] &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结？&lt;/h2&gt;
&lt;p&gt;如果转移方程能表示成：$f_i=\max_{j&amp;#x3C;i}{{a_ib_j+c_j+d_i}}$ 的形式（或 $\min$），并且 $a_i$ 关于 $i$ 有单调性，就都可以用斜率优化。&lt;/p&gt;
&lt;p&gt;斜率优化能把 $\mathcal{O}(n^2)$ 降成 $\mathcal{O}(n)$.&lt;/p&gt;
&lt;p&gt;数形结合。&lt;/p&gt;
&lt;p&gt;代码短。&lt;/p&gt;</content:encoded></item><item><title>【洛谷-P2900】Land Acquisition</title><link>https://www.tonyyin0418.com/blog/oi-solution/p2900</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p2900</guid><description>算法题解</description><pubDate>Thu, 25 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;翻译的不是很好，所以我简化一下题面。&lt;/p&gt;
&lt;p&gt;给定 $n$ 个矩阵的宽 $w$ 和高 $h$，每次选一个矩阵&lt;strong&gt;集合&lt;/strong&gt;，代价为集合中的 $\max{{w}}\times \max{{h}}$。&lt;/p&gt;
&lt;p&gt;任意两次之间没有重复的矩阵，求取完所有矩阵的&lt;strong&gt;最小代价&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;矩阵的两个参数为 $w, h$，分别对应下图中的宽和高。&lt;/p&gt;
&lt;p&gt;函数 $\operatorname{slope}(i, j)$ 表示 $(x_i, y_i)$ 与 $(x_j, y_j)$ 构成的直线的&lt;strong&gt;斜率&lt;/strong&gt;，$x, y$ 的含义在下面会说。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;注意到对于两个矩阵 $i, j$，如果 $w_i\leq w_j$ 并且 $h_i\leq h_j$，那么 $i$ 和 $j$ 一定可以放到一组，$i$ &lt;strong&gt;对答案没有贡献&lt;/strong&gt;，不用考虑，可以直接删去。&lt;/p&gt;
&lt;p&gt;比如下图当中的 $1$ 和 $2$. 矩阵 $2$ 能完全被 $1$ 包含，所以 $2$ 没有贡献，可以删去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/02/25/53d870c086029.png&quot; alt=&quot;P2900-1&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是考虑先删除所有 $i$ 这样的矩阵。&lt;/p&gt;
&lt;p&gt;具体做法就是：按照 $h$ 为第一关键字，$w$ 为第二关键字&lt;strong&gt;从大到小&lt;/strong&gt;排序，之后&lt;strong&gt;双指针&lt;/strong&gt;扫一遍即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;//a已经降序排序
int t = 0;
for(int i = 1; i &amp;#x3C;= n; i++) {
	if(a[i].w &gt; a[t].w) a[++t] = a[i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上图，剩下的只有 $1, 3$ 号矩阵。&lt;/p&gt;
&lt;p&gt;删完之后剩下的就是一个，$h$ 严格&lt;strong&gt;单调递减&lt;/strong&gt;，$w$ 严格&lt;strong&gt;单调递增&lt;/strong&gt;的序列。考虑 $DP$ 解决。&lt;/p&gt;
&lt;h2&gt;DP&lt;/h2&gt;
&lt;p&gt;设 $f_i$ 为新序列中，取走前 $i$ 个矩阵的&lt;strong&gt;最小&lt;/strong&gt;花费，$w$ 为宽（单调递增），$h$ 为高（单调递减），那么有转移方程：
$$
f_i=\min_{j=0}^{i}{{f_{j}+w_ih_{j+1}}}
$$
相当于，把 $j+1$ 到 $i$ 这一段合为一组取走。由于单调性，所以这一组的 $\max{{w}}=w_i$，$\max{{h}}=h_{j+1}$.&lt;/p&gt;
&lt;p&gt;复杂度 $\mathcal{O}(n^2)$。这显然可以斜率优化。&lt;/p&gt;
&lt;h2&gt;斜率优化&lt;/h2&gt;
&lt;p&gt;我们目前优化的目的，是尽快地找到 $f_i$ 到底是由哪个 $j$ 转移过来的，也就是尽快找到最小化 $f_i$ 的继承状态。&lt;/p&gt;
&lt;p&gt;为了表示方便，设 $j$ 为能&lt;strong&gt;使 $f_i$ 最小化的继承状态&lt;/strong&gt;，所以有：
$$
f_i=f_{j}+w_ih_{j+1}
$$
考虑一次函数斜截式 $y=kx+b$ 或 $b=y-kx$，将状态转移方程变换为这个形式：
$$
f_{j}=-w_ih_{j+1}+f_i
$$
其中&lt;strong&gt;变量 $x, y$ 与 $j$ 有关，$b, k$ 与 $j$ 无关&lt;/strong&gt;。且要求 $x$ 随 $j$ 单调递增，$b$ 中包含 $f_i$（斜率优化基本操作）。&lt;/p&gt;
&lt;p&gt;所以可以构造出直线方程为：
$$
\left{
\begin{array}{lr}
y = f_j\
k = w_i\
x = -h_{j+1}\
b = f_i
\end{array}
\right.
$$&lt;/p&gt;
&lt;p&gt;我们把 $(x, y)$ 看成平面直角坐标系上的点。那么现在 $(x_j, y_j)$ 的意义就是：&lt;strong&gt;直线 $y=kx+b$ 上的一个点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;又因为我们的目的是最小化 $f_i$，在上面的表示当中，$f_i$ 只与直线的截距 $b$ 有关。所以显然地，问题转化为：&lt;strong&gt;如何选取 $j$ ，才能使得过点 $(x_j, y_j)$ 的直线的截距 $b$ 最小。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pic.tonyyin.top/2021/02/25/39c3696467e33.png&quot; alt=&quot;20190701195640154&quot;&gt;&lt;/p&gt;
&lt;p&gt;显然，对于一个斜率 $k$ 来说，最优解 $j$ 就是从左往右枚举到的&lt;strong&gt;第一个 $\operatorname{slope}(j, j+1)&gt;k$ 的点&lt;/strong&gt;。（其中 $\operatorname{slope}$ 的定义在上面有说过）&lt;/p&gt;
&lt;p&gt;再考虑单调性： $x$ 随 $j$ 单调递增，$k$ 随 $i$ 单调递增，要使截距 $b=f_i$ 最小。&lt;/p&gt;
&lt;p&gt;显然，所有的&lt;strong&gt;最优决策点&lt;/strong&gt; $j$ 都在&lt;strong&gt;下凸包&lt;/strong&gt;上，也就是上图中的折线。&lt;/p&gt;
&lt;p&gt;所以单调队列维护下凸包即可：&lt;/p&gt;
&lt;p&gt;设单调队列（递增）为 $q$，队首为 $head$，队尾为 $tail$，当前要转移 $i$ 号点，那么具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{head}, q_{head+1})\leq w_i=k_i$ 时，$head$++，（保证最优解）&lt;/li&gt;
&lt;li&gt;此时的 $q_{head}$ 就是&lt;strong&gt;最优转移点&lt;/strong&gt;，根据它和转移方程计算出 $f_i$，&lt;/li&gt;
&lt;li&gt;当 $\operatorname{slope}(q_{tail-1}, q_{tail})\geq\operatorname{slope}(q_{tail}, i)$ 时，$tail$--，（维护&lt;strong&gt;下凸包&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;在队尾插入 $i$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define X(i) (-a[i+1].h)
#define Y(i) (f[i])
#define int long long
using namespace std;
const int MAXN = 5e4 + 10;
int n;
struct Node{
	int w, h;
} a[MAXN];
int f[MAXN];
bool cmp(Node i, Node j) {
	return (i.h &gt; j.h) || (i.h == j.h &amp;#x26;&amp;#x26; i.w &gt; j.w);
}
double slope(int i, int j) {
	return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
signed main() {
	scanf(&quot;%lld&quot;, &amp;#x26;n);
	for(int i = 1; i &amp;#x3C;= n; i++) {
		scanf(&quot;%lld%lld&quot;, &amp;#x26;a[i].w, &amp;#x26;a[i].h);
	}
	sort(a + 1, a + n + 1, cmp);
	int t = 0;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		if(a[i].w &gt; a[t].w) a[++t] = a[i];
	}
	n = t;
	head = tail = 1;
	for(int i = 1; i &amp;#x3C;= n; i++) {
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[head], q[head + 1]) &amp;#x3C;= a[i].w) {
			head++;
		}
		int j = q[head]; f[i] = f[j] + a[i].w * a[j + 1].h;
		while(head &amp;#x3C; tail &amp;#x26;&amp;#x26; slope(q[tail - 1], q[tail]) &gt;= slope(q[tail], i)) {
			tail--;
		}
		q[++tail] = i;
	}
	cout &amp;#x3C;&amp;#x3C; f[n] &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>网络流-最大流</title><link>https://www.tonyyin0418.com/blog/oi-notes/max-flow</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/max-flow</guid><description>网络流学习笔记，这里介绍了几种求最大流的算法。 </description><pubDate>Wed, 20 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;网络流简介&lt;/h1&gt;
&lt;h2&gt;容量网络&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;网络&lt;/strong&gt;：指一个有向图 $G(V,E)$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容量&lt;/strong&gt;：在 $G$ 中，称每条边 $(u, v)$ 的权值 $c(u, v)$，为这条边的&lt;strong&gt;容量&lt;/strong&gt;；特殊的，若 $(u,v)\notin E$，$c(u, v)=0$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源点和汇点&lt;/strong&gt;：$V$ 中的两个特殊点。一般记 $s$ 为源点，$t$ 为汇点，$s\neq t$.&lt;/p&gt;
&lt;h2&gt;流&lt;/h2&gt;
&lt;h4&gt;流函数&lt;/h4&gt;
&lt;p&gt;网络 $G$ 的&lt;strong&gt;流函数&lt;/strong&gt;是定义在二元组( $u\in V, v\in V$ )上的实数函数 $f(u, v)$.
$$
f(u,v)=
\left{
\begin{aligned}
&amp;#x26;f(u,v),&amp;#x26;(u,v)\in E\
&amp;#x26;-f(v,u),&amp;#x26;(v,u)\in E\
&amp;#x26;0,&amp;#x26;(u,v)\notin E,(v,u)\notin E
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;对于 $(u, v)\in E$，$f(u, v)$ 称为边的&lt;strong&gt;流量&lt;/strong&gt;，$c(u, v)-f(u, v)$ 称为边的&lt;strong&gt;剩余流量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;整个&lt;strong&gt;网络的流量&lt;/strong&gt;为 $\sum_{(s, v)\in E} f(s, v)$，也就是&lt;strong&gt;从源点出发的所有流量的和&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;性质&lt;/h4&gt;
&lt;p&gt;$f(u, v)$ 满足以下三条性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容量限制：对于每条边，流经该边的流量不得超过该边的容量，即 $f(u,v)\leq c(u,v)$;&lt;/li&gt;
&lt;li&gt;斜对称性：每条边的流量与其相反边的流量之和为 $0$，即 $f(u,v)=-f(v,u)$;&lt;/li&gt;
&lt;li&gt;流守恒性：从源点流出的流量等于汇点流入的流量，即 $\forall x\in V\setminus{s,t},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;最大流&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;在一个流量网络中，求从源点 $s$ 流向汇点 $t$ 的最大流量。流量定义为：&lt;strong&gt;从源点出发的所有流量的和&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Ford-Fulkerson 增广路算法&lt;/h2&gt;
&lt;p&gt;通过寻找增广路来更新最大流。注意：这里的增广路与二分图中的增广路定义不同。&lt;/p&gt;
&lt;h3&gt;残量网络&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;残量&lt;/strong&gt;：给定容量网络 $G(V, E)$ 及可行流 $f$，$(u, v)$ 上的残量记为 $c_f(u, v)=c(u, v)-f(u, v)$.&lt;/p&gt;
&lt;p&gt;对于每条边而言，残量为这条边可以增加的容量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;残量网络&lt;/strong&gt;：对于容量网络 $G(V, E)$ 及其上的网络流 $f$，$G$ 关于 $f$ 的残量网络为 $G_f(V_f, E_f)$. $(u, v)\in E_f$ 的边权记为 $c_f(u, v)$ . 残量网络 $G_f$ 满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$V_f=V$&lt;/li&gt;
&lt;li&gt;对于任意 $(u, v)\in E$，如果 $f(u, v) &amp;#x3C; c(u, v)$，那么 $(u, v)\in E_f$ 且 $c_f(u, v) = c(u, v) - f(u, v)$.&lt;/li&gt;
&lt;li&gt;对于任意 $(u, v)\in E$，如果 $f(u, v) &gt; 0 $，那么 $(v, u)\in E_f$ 且 $c_f(v, u)=f(u, v)$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通俗的来说，比如对于一条边 $(u, v)$，$f(u, v)=3$，$c(u, v)=5$。这条边的流量最多可以再增加 $2$，所以残量网络中 $c_f(u, v)=2$；同时，这条边的流量最多可以减少 $3$，又因为 &lt;strong&gt;$v$ 向 $u$ 增加流量，等价于 $u$ 向 $v$ 减少流量&lt;/strong&gt;，所以在残量网路中的体现就是 $c_f(v, u)=3$。&lt;/p&gt;
&lt;p&gt;因此，残量大于 $0$ 的边有可能不在原图 $G$ 中（但这条边的反向边在原图 $G$ 中）。&lt;/p&gt;
&lt;h3&gt;增广路&lt;/h3&gt;
&lt;p&gt;在原图 $G$ 中，若一条从源点到汇点的路径满足：路径上所有边的剩余容量都大于 $0$，那么这条路被称为增广路。&lt;/p&gt;
&lt;p&gt;或者说，在残量网络 $G_f$ 中，能找到的从源点到汇点的路径，都是增广路。&lt;/p&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/graph/flow&quot;&gt;网络流简介 - OI Wiki&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jisuanke.com&quot;&gt;网络流 - 计蒜客 Level 6 课程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://github.com/MadMaxChow/VLOOK&lt;/p&gt;
&lt;p&gt;https://www.jisuanke.com/course/4728&lt;/p&gt;</content:encoded></item><item><title>【ZROI-20普转提-Day1-T4】魔法师</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-20pzt-day1-t4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-20pzt-day1-t4</guid><description>算法题解</description><pubDate>Tue, 12 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;【ZROI-20普转提-Day1-T4】魔法师&lt;/p&gt;
&lt;h1&gt;$\rm{Description}$&lt;/h1&gt;
&lt;p&gt;有 $m$ 次询问，每次询问给定一次操作。操作分为两种：向集合 $A$ 中添加一个物品或从集合 $A$ 中删除一个物品。对于每个物品，有两个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;物品的类别 $t_i$&lt;/li&gt;
&lt;li&gt;物品的威力 $p_i$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在要从集合 $A$ 中选取一个子集 $B$，使得 $B$ 中物品的威力和最大，且 $B$ 满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于每个类别 $t_i$，最多选取 $t_i$ 本书；&lt;/li&gt;
&lt;li&gt;一共最多能够选取 $n$ 本书（题目给定 $n$）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在每次询问之后，输出最大威力和。&lt;/p&gt;
&lt;h1&gt;$\rm{Solution}$&lt;/h1&gt;
&lt;h2&gt;$\rm{Subtask}$ $\rm{1}$&lt;/h2&gt;
&lt;p&gt;容易想到使用 &lt;code&gt;multiset&lt;/code&gt; 进行暴力，期望得分 $30$ 分。&lt;/p&gt;
&lt;h2&gt;$\rm{Subtask}$ $\rm{2}$&lt;/h2&gt;
&lt;p&gt;考虑如何处理“&lt;strong&gt;最多取 $t_i$ 个，最多取 $n$ 个&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;对于每种物品 $t$，开两个 &lt;code&gt;multiset&lt;/code&gt;，分别包含威力在前 $t$ 大的元素和其他元素。&lt;/p&gt;
&lt;p&gt;显然对于每个物品 $i$，只有在自己的种类中，威力排在前 $t_i$，才有可能在集合 $B$ 中。&lt;/p&gt;
&lt;p&gt;于是再开两个全局的 &lt;code&gt;multiset&lt;/code&gt;，第一个是集合 $B$，第二个是所有前 $t_i$ 大的元素中，不在 $B$ 中的元素。&lt;/p&gt;
&lt;p&gt;此时对于一个添加操作来说，先判断是不是前 $t_i$ 大；若是，则看能不能添加到 $B$ 集合中；若能，则更新答案。&lt;/p&gt;
&lt;p&gt;对于一个删除操作来说，同理。先判断是不是前 $t_i$ 大，若是，则把小的集合中最大的元素放到前 $t_i$ 大中；若同时还在集合 $B$ 中，则处理方式类似，只不过还要更新答案。&lt;/p&gt;
&lt;h1&gt;$\rm{Code}$&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
inline int read() {
	int ret = 0; char ch = getchar();
	while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) ch = getchar();
	while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) {
		ret = ret * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	return ret;
}
const int MAXN = 3e5 + 10;
int n, m;
multiset&amp;#x3C;int&gt; a_big[MAXN], a_small[MAXN];
multiset&amp;#x3C;int&gt; big, small;
//用 a_big[i] 存目前有的第 i 种书中，前i大的数值，升序
//用 a_small[i] 存目前有的第 i 种书中，其他的数值，升序
//用 big 存最终取出的最多 n 本书，一定从 a_big 中取出，升序
//用 small 存其他的数值
int maxt, ans;
void out(int x) {
	if(x &gt;= 10) out(x / 10);
	putchar(x % 10 + &apos;0&apos;);
}
void add(int x) {//将数值 x 添加到 big/small 中
	if(big.size() &amp;#x3C; n) {//如果big中还能加，就直接加
		big.insert(x);
		ans += x;
	} else {
		//否则先找到big中的最小值mmin
		int mmin = *big.begin();
		if(x &gt; mmin) {//如果x能顶替big中的一个元素，那么顶替
			ans -= mmin; ans += x;
			small.insert(mmin);
			big.erase(big.begin());
			big.insert(x);
		} else {//否则添加到small
			small.insert(x);
		}
	}
}
void del(int x) {//将数值 x 从 big/small 中删除
	if(big.find(x) != big.end()) { //如果在big里面能找到
		ans -= x;
		big.erase(big.find(x));
		if(!small.empty()) {
			//找到small里面最大的，放到big里，之后把x从big中删除
			auto it = small.end(); it--;
			int mmax = *it;
			small.erase(it);
			big.insert(mmax);
			ans += mmax;
		}
	} else { //不在big中，就一定在small中，直接删
		small.erase(small.find(x));
	}
}
signed main() {
	n = read(); m = read();
	for(int i = 1; i &amp;#x3C;= m; i++) {
		
		int op = 0;
		char ch = getchar();
		while(ch &amp;#x3C; &apos;A&apos; || ch &gt; &apos;Z&apos;) ch = getchar();
		if(ch == &apos;B&apos;) op = 1;
		else if(ch == &apos;R&apos;) op = 2;
		int t = read(), p = read(); maxt = max(maxt, t);
		
		if(op == 1) { //Borrow
			//要把元素p加入进去
			if(a_big[t].size() &amp;#x3C; t) {//如果有空余就加
				a_big[t].insert(p);
				add(p);
			} else {
				//否则找到big里的最小值，和p进行比较
				int mmin = *a_big[t].begin();
				if(p &gt; mmin) {//能把big中的一个元素顶替下来
					del(mmin);
					a_small[t].insert(mmin);
					a_big[t].erase(a_big[t].begin());
					add(p);
					a_big[t].insert(p);
				} else {//不能，添加到small
					a_small[t].insert(p);
				}
			}
		} else { //Return
			if(a_big[t].find(p) != a_big[t].end()){
				//如果在big里面，就找到small中的最大值，放到big里面，然后删掉p
				a_big[t].erase(a_big[t].find(p));
				del(p);
				if(!a_small[t].empty()) {
					auto it = a_small[t].end(); it--;
					int mmax = *it;
					a_small[t].erase(it);
					a_big[t].insert(mmax);
					add(mmax);
				}
			} else {//在small里面直接删
				a_small[t].erase(a_small[t].find(p));
			}
		}
		out(ans); putchar(&apos;\n&apos;);
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>博弈论基础</title><link>https://www.tonyyin0418.com/blog/oi-notes/game-theory</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/game-theory</guid><description>学习笔记，内容包含：巴什博弈、威佐夫博弈、必胜必败分析、SG 函数、组合博弈游戏、Nim 类型石子游戏、相关例题</description><pubDate>Sun, 03 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;概述&lt;/h1&gt;
&lt;p&gt;博弈论，可以定义为两人或多人面对一定的规则约束下，根据所掌握的信息，同时或先后，一次或多次地，从各自允许的行为中选择最优策略，通常要使自身收益最大化。&lt;/p&gt;
&lt;h1&gt;囚徒困境&lt;/h1&gt;
&lt;p&gt;两名罪犯A, B分开囚禁在两个房间内，互相之间不能交流。&lt;/p&gt;
&lt;p&gt;每名罪犯可以检举对方犯罪，也可以保持沉默。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若A检举B，且B沉默，则B被判20年。&lt;/li&gt;
&lt;li&gt;若B检举A，且A沉默，则A被判20年。&lt;/li&gt;
&lt;li&gt;若A，B都检举对方，同时被判8年。&lt;/li&gt;
&lt;li&gt;若A，B都沉默，同时被判1年。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|  &lt;strong&gt;A/B&lt;/strong&gt;  | &lt;strong&gt;A检举&lt;/strong&gt; | A沉默 |
| :-------: | :-------: | :---: |
| &lt;strong&gt;B检举&lt;/strong&gt; |    8/8    | 10/0  |
| &lt;strong&gt;B沉默&lt;/strong&gt; |   0/10    |  1/1  |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;博弈：对于每个个体来讲都要是最优的。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;P1288 取数游戏Ⅱ&lt;/h1&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;取数游戏。给出一个环，每条边都有为负边权，所有边中至少有一个 $0$.&lt;/p&gt;
&lt;p&gt;将一枚硬币放在环的一个节点上，两个玩家以这个放硬币的节点为起点开始游戏，轮流进行操作，操作方案如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择当前硬币连出的两条边中的一条，要求这条边的权值为正；&lt;/li&gt;
&lt;li&gt;将选取的这条边的权值，减少到任意一个，小于原本权值的非负整数；&lt;/li&gt;
&lt;li&gt;将硬币移动到选取边的另一侧。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果轮到一个玩家，硬币的两条边权都是 $0$，那么输掉游戏，比如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;C:%5CUsers%5Cywdadmin%5COneDrive%5C%E5%8D%9A%E5%BC%88%E8%AE%BA%E5%9F%BA%E7%A1%800105.assets%5C93.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;p&gt;显然每次操作，把边权直接变为 $0$ 最优。&lt;/p&gt;
&lt;p&gt;所以分别看起点的两端，如果它们的其中一个到第一个 $0$ 的距离为奇数，那么先手胜，反之后手胜。&lt;/p&gt;
&lt;h1&gt;巴什博奕&lt;/h1&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;有 $n$ 个物品，&lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;两人轮流从物品中拿走一部分，&lt;strong&gt;每次至少拿 $1$ 个，最多拿 $𝑚$ 个&lt;/strong&gt;，拿走最后一个的人获胜。是否有必胜策略，若有，则求出策略。&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;p&gt;可以想到，当物品的总量 $n=(m+1)r+s$ 时，&lt;code&gt;A&lt;/code&gt;先拿走 $s$ 个物品，之后每次若&lt;code&gt;B &lt;/code&gt;取走 $k$ 个，则 &lt;code&gt;A&lt;/code&gt;对应的取走 $m+1-k$ 个，这样&lt;code&gt;A&lt;/code&gt;显然能够取得胜利。&lt;/p&gt;
&lt;p&gt;若物品总量为 $n=(m+1)r$ ，此时B用同样的策略即可，B必胜。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;iostream&gt;
using namespace std;
int main() {
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;
    if(n % (m + 1) == 0) {
        cout &amp;#x3C;&amp;#x3C; &quot;后手胜&quot; &amp;#x3C;&amp;#x3C; endl;
    } else {
        cout &amp;#x3C;&amp;#x3C; &quot;先手胜&quot; &amp;#x3C;&amp;#x3C; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;威佐夫博弈&lt;/h1&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;有两堆，若干个物品，两个人轮流&lt;strong&gt;从某一堆或从两堆中取同样多的物品&lt;/strong&gt;，规定每次最少拿走一个，最多不限制，最后拿光的人获胜。&lt;/p&gt;
&lt;h2&gt;$\rm{Analysis}$&lt;/h2&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;A&lt;/code&gt; 是否有必胜策略，可以这样分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用有序数对 $(a, b)$ 表示两堆物品的数量状态；&lt;/li&gt;
&lt;li&gt;$(0, 0)$ 是 $A$ 的必败策略，因为此时 $A$ 已输；&lt;/li&gt;
&lt;li&gt;若局面能够移动到 $B$ 无必胜策略的局面，那么这个局面是 $A$ 的&lt;strong&gt;必胜态&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;若局面无论怎么移动，都会变成 $B$ 的必胜太，那么这个局面是 $A$ 的&lt;strong&gt;必败态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过枚举，可以发现 $A$ 的必败态有：
$$
(0, 0),(1, 2),(3, 5),(4, 7),(6, 10),(8, 13),(9, 15),(11, 18),(12, 20)\ldots
$$
用有序数对序列$(a_0, b_0),(a_1, b_1),(a_2, b_2)\ldots$表示上面的必败态，可以看出，满足：$a_0=b_0=0$ 且 $b_i=a_i+i$，$a_i$ 是之前没有出现过的最小自然数。&lt;/p&gt;
&lt;h2&gt;$\rm{Demonstrate}$&lt;/h2&gt;
&lt;h4&gt;前置命题： $a_{n+1}$ 是前 $n$ 组必败态中没有出现过的最小正整数。&lt;/h4&gt;
&lt;h4&gt;证明命题： $\forall n, b_n=a_n+n$.&lt;/h4&gt;
&lt;p&gt;考虑归纳证明。若前 $k$ 个必败态分别为 $(a_1, a_1+1), (a_2, a_2+2), \cdots, (a_k, a_k+k)$，下证：第 $k+1$ 个必败态为 $(a_{k+1}, a_{k+1}+k+1)$.&lt;/p&gt;
&lt;p&gt;从这个必败态出发，一共可能走向三类状态：从第一堆拿走，从第二堆拿走，或者两堆同时拿走，只需证明这三类都是必胜态。&lt;/p&gt;
&lt;p&gt;**情况一：**从第一堆拿走一些。&lt;/p&gt;
&lt;p&gt;因为任意一个比 $a_{k+1}$ 小的数都在之前的必胜态中出现过，第一堆变少之后，只需要把第二堆拿到第一堆对应的必败态。所以对于出现的新的状态，都可以直接指向必败态。&lt;/p&gt;
&lt;p&gt;**情况二：**从第二堆拿走一些。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;拿完之后第二堆仍然大于第一堆。此时可以表示成 $(a_{k+1}, a_{k+1}+m), (m&amp;#x3C;k+1)$，那么两堆同时拿掉一些之后，变成 $(a_m, a_m+m)$.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;拿完之后第二堆小于第一堆，设其拿成了 $(a_{k+1}, m)$，则 $m&amp;#x3C;a_{k+1}$ 一定属于之前的一个必败态，将第一堆拿到第二堆对应的必败态即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;情况三&lt;/strong&gt;：两堆同时拿走一些。&lt;/p&gt;
&lt;p&gt;设拿完之后的状态是 $(m, m+k+1)$。因为 $m&amp;#x3C;a_{k+1}$，所以第一堆一定属于之前的一个必败态，将第二堆拿到第一堆对应的必败态即可。&lt;/p&gt;
&lt;p&gt;所以现在已经得到了数列关系式 $b_n=a_n+n$.&lt;/p&gt;
&lt;h2&gt;$\rm{Attribute}$&lt;/h2&gt;
&lt;p&gt;在这个问题中，我们称必败态为奇异局势，记为 $(a_k, b_k)$ 那么有如下性质：&lt;/p&gt;
&lt;h4&gt;1. 任何自然数都包含在一个且仅一个奇异局势中&lt;/h4&gt;
&lt;p&gt;因为 $a_k$ 是之前没有出现过最小自然数，所以所有自然数都会被包含到奇异局势当中。&lt;/p&gt;
&lt;p&gt;由于 $a_k&gt;a_{k-1}$，所以 $b_k=a_k+k&gt;a_{k-1}+k&gt;a_{k-1}+k-1=b_{k-1}&gt;a_{k-1}$。整理后，有 $b_k &gt; b_{k-1}&gt;a_{k-1}$，所以不存在自然数被两个奇异局势包含。&lt;/p&gt;
&lt;h4&gt;2. 任意操作都可以将奇异局势变为非奇异局势&lt;/h4&gt;
&lt;p&gt;也就是说，&lt;strong&gt;必败态的后继一定都是必胜态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于奇异局势 $(a_k, b_k)$，如果只改变其中一个数，那么由性质$1$，不可能是另一个奇异局势；&lt;/p&gt;
&lt;p&gt;如果同时改变两个数，那么这两个数的差不变，由于 $b_k=a_k+k$，对于一个差值，只可能对应一个奇异局势，所以变化后也不可能是另一个奇异局势。&lt;/p&gt;
&lt;h4&gt;3. 可以将非奇异局势变为奇异局势&lt;/h4&gt;
&lt;p&gt;根据奇异局势满足 $b_k=a_k+k$，可以分类讨论进行证明。&lt;/p&gt;
&lt;p&gt;具体地，对于任意的局势 $(a, b)$，&lt;/p&gt;
&lt;p&gt;若 $a=b$，那么同时取走 $a$ 个物品即可变为 $(0, 0)$；&lt;/p&gt;
&lt;p&gt;若 $a$ 是一个奇异局势中的 $a_k$，且 $b&gt;b_k$，那么从 $b$ 堆中取出 $b-b_k$个物品，变为 $(a_k, b_k)$.&lt;/p&gt;
&lt;p&gt;若 $a$ 是一个奇异局势中的 $a_k$，且 $b&amp;#x3C;b_k$，那么从两堆中同时拿走 $a-a_{b-a}$ 个物品。&lt;/p&gt;
&lt;p&gt;若 $a$ 不属于任何一个奇异局势中，那么 $b$  一定是一个奇异局势中的 $b_k=a_k+k$，此时有两种情况：&lt;/p&gt;
&lt;p&gt;如果 $a&gt;a_k$，从第一堆中拿走多余的 $a-a_k$.&lt;/p&gt;
&lt;p&gt;如果 $a&amp;#x3C;a_k$，如果 $a=a_j (j&amp;#x3C;k)$，就从第二堆中拿走 $b-b_j$；如果 $a=b_j(j&amp;#x3C;k)$，就从第二堆里面拿走 $b-a_j$.&lt;/p&gt;
&lt;h2&gt;$\rm{Conclusion}$&lt;/h2&gt;
&lt;p&gt;快速判断局势 $(a, b)$ 是否是奇异局势的方法。&lt;/p&gt;
&lt;h3&gt;Beatty定理&lt;/h3&gt;
&lt;p&gt;设 $a, b$ 是正无理数，且 $\frac{1}{a}+\frac{1}{b}=1$，记 $P=\lfloor{na}\rfloor, Q=\lfloor{nb}\rfloor$，$n$ 为任意正整数。&lt;/p&gt;
&lt;p&gt;那么 $P\cap Q=\emptyset$ 且 $P\cup Q=N^{*}$.&lt;/p&gt;
&lt;p&gt;也就是说：取两个正无理数 $\frac{1}{\alpha}+\frac{1}{\beta}=1$，构造两个数列 $a_n=\lfloor\alpha n\rfloor$ 和 $b_n=\lfloor\beta n\rfloor$，那么两个数列单调递增，并且每个正整数在两个数列当中出现且仅出现一次。&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;p&gt;发现奇异局势和 Beatty 定理的条件很像，考虑计算出 $\alpha$ 和 $\beta$.&lt;/p&gt;
&lt;p&gt;两个数列 $a_n$ 和 $b_n$ 就是奇异局势中的 $(a_n, b_n)$.&lt;/p&gt;
&lt;p&gt;$a_n+n=\lfloor{\alpha\cdot n}\rfloor+n=\lfloor{(\alpha+1)\cdot n}\rfloor=\lfloor{\beta\cdot n}\rfloor$.&lt;/p&gt;
&lt;p&gt;所以 $\alpha+1=\beta$&lt;/p&gt;
&lt;p&gt;所以 $\frac{1}{\alpha}+\frac{1}{\beta}=\frac{1}{\alpha}+\frac{1}{\alpha+1}=1$.&lt;/p&gt;
&lt;p&gt;解得 $\alpha=\frac{\sqrt{5}+1}{2}$.&lt;/p&gt;
&lt;p&gt;所以 $a_n=\lfloor\frac{\sqrt{5}+1}{2}n\rfloor$，$b_n=\lfloor(\frac{\sqrt{5}+1}{2}+1)n\rfloor=\lfloor\frac{3+\sqrt{5}}{2}n\rfloor=\lfloor(\frac{\sqrt{5}+1}{2})^2n\rfloor$.&lt;/p&gt;
&lt;p&gt;所以根据 Beatty 定理，奇异局势的通项公式为：$a_n=\lfloor(\frac{\sqrt{5}+1}{2})n\rfloor$，$b_n=\lfloor(\frac{\sqrt{5}+1}{2})^2n\rfloor$.&lt;/p&gt;
&lt;p&gt;而 $\frac{\sqrt{5}+1}{2}$ 是黄金比，所以威佐夫博弈的奇异局势与黄金比相关。&lt;/p&gt;
&lt;h3&gt;判断方法&lt;/h3&gt;
&lt;p&gt;根据奇异局势的性质，对于需要判断的局势 $(a, b), (a&amp;#x3C;b)$，设 $k=b-a$，可以计算出 $a_k$ 和 $b_k$，直接和 $a,b$ 进行比较即可。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;
bool eq(double a, double b) {
	if(abs(a - b) &amp;#x3C; 1e-4) return true;
	else return false;
}
int main() {
	double a, b; cin &gt;&gt; a &gt;&gt; b;
	if(a &gt; b) swap(a, b);
	double t = (1.0 + sqrt(5.0)) / 2.0;
	double k = b - a;
	if(eq(floor(k * t), a) &amp;#x26;&amp;#x26; eq(floor(k * t * t), b)) {
		cout &amp;#x3C;&amp;#x3C; 0 &amp;#x3C;&amp;#x3C; endl;
	} else {
		cout &amp;#x3C;&amp;#x3C; 1 &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;必胜必败分析&lt;/h1&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;更明确的定义概念：&lt;/p&gt;
&lt;h4&gt;必胜点（N）&lt;/h4&gt;
&lt;p&gt;先手必胜点。&lt;/p&gt;
&lt;h4&gt;必败点（P）&lt;/h4&gt;
&lt;p&gt;先手必败点。&lt;/p&gt;
&lt;h4&gt;性质&lt;/h4&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;/p&gt;
&lt;ol&gt;
&lt;li&gt;将终结点标为必败点；&lt;/li&gt;
&lt;li&gt;将所有一步操作进入必败点的位置标为必胜点；&lt;/li&gt;
&lt;li&gt;如果从某一个点开始，一步操作后只能进入必胜点，那么标记为必败点；&lt;/li&gt;
&lt;li&gt;重复步骤2， 3.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;记忆化搜索的过程，可以画出&lt;strong&gt;博弈树&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;h3&gt;描述&lt;/h3&gt;
&lt;p&gt;一个 $4$ 行 $3$ 列的矩阵，有一个小球放在左上角。两个人依次移动小球，每次只能：&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;p&gt;最后无路可走的人输。两人按最优策略，问谁必胜。&lt;/p&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;直接用 $4$ 行 $3$ 列的矩阵表示必胜必败情况。用&lt;code&gt;*&lt;/code&gt;表示情况不确定，&lt;code&gt;P&lt;/code&gt;表示必败，&lt;code&gt;N&lt;/code&gt;表示必胜。&lt;/p&gt;
&lt;p&gt;首先标记最终的点是必败点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;***
***
***
**P
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后推第四行。第四行的所有点，只能往右走，所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;***
***
***
PNP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看第三行。$(3, 3)$ 可以往下走到必败，所以是必胜点。$(3, 2)$ 可以往右下走到必败，所以是必胜点，$(3, 1)$ 同理，所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;***
***
NNN
PNP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，可以算出最后两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NNN
PNP
NNN
PNP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，先手必胜。&lt;/p&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;p&gt;两个人从一堆 $n$ 个物品中轮流取物品，每次取 $1, 3, 4$ 个物品，最后没法取的人输。分析必胜必败。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool win[1001], vis[1001];//win[i]表示剩余i个物品时的必胜必败情况
void init() {
    win[0]=false;
    vis[0]=true;
}
void dfs(int x){
    if(vis[x]){
        return;
    }
    if(x&gt;=1){
        dfs(x-1);
    }
    if(x&gt;=3){
        dfs(x-3);
    }
    if(x&gt;=4){
        dfs(x-4);
    }
    win[x]=false;
    if(x&gt;=1&amp;#x26;&amp;#x26;!win[x-1]){
        win[x]=true;
    }
    if(x&gt;=3&amp;#x26;&amp;#x26;!win[x-3]){
        win[x]=true;
    }
    if(x&gt;=4&amp;#x26;&amp;#x26;!win[x-4]){
        win[x]=true;
    }
    vis[x]=true;
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;SG函数&lt;/h1&gt;
&lt;h2&gt;组合博弈游戏&lt;/h2&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;li&gt;两个人轮流操作&lt;/li&gt;
&lt;li&gt;一般情况下，当一方无法操作时，无法操作者输，游戏结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我是真找不到别的定义了……&lt;/p&gt;
&lt;p&gt;对于任意一个公平的组合博弈游戏（ICG），我们将每个可能的局面当成图论当中的一个点。&lt;/p&gt;
&lt;p&gt;在这个图中的连边规则是：把局面向&lt;strong&gt;这个局面经过操作后可能得到的所有局面&lt;/strong&gt;连边，构成一个有向图。如果这个游戏对于任意的局面 $A$，都不能通过一些操作，回到这个局面 $A$，那么构成的图是一个&lt;strong&gt;有向无环图&lt;/strong&gt;（DAG）。&lt;/p&gt;
&lt;h2&gt;SG函数&lt;/h2&gt;
&lt;p&gt;定义一个 $mex$ 函数，表示对于一个集合，不属于该集合的最小非负整数。比如 $mex{0, 1, 2, 4}=3$.&lt;/p&gt;
&lt;p&gt;还是在这个博弈游戏中：两个人从一堆 $n$ 个物品中轮流取物品，每次取 $1, 3, 4$ 个物品，最后没法取的人输。&lt;/p&gt;
&lt;p&gt;我们对每个状态进行分级，终结态的级别是 $0$. 如果一个状态能够变为 $0$ ~ $n-1$ 级的状态，那么这个状态是 $n$ 级状态。&lt;/p&gt;
&lt;p&gt;状态 $1$ 能够变成 $0$ 级状态，称它为 $1$ 级状态；&lt;/p&gt;
&lt;p&gt;状态 $2$ 只能变成 $1$ 级，不能变成 $0$ 级，所以它的级别为 $0$；&lt;/p&gt;
&lt;p&gt;状态 $3$ 可以变为 $0$ 级，级别为 $1$；&lt;/p&gt;
&lt;p&gt;状态 $4$ 可以变为 $0,1$ 级，级别为 $2$；&lt;/p&gt;
&lt;p&gt;状态 $5$ 可以变为 $0,1,2$ 级，级别为 $3$.&lt;/p&gt;
&lt;p&gt;在博弈游戏当中，&lt;strong&gt;SG函数&lt;/strong&gt;定义为：$\operatorname{sg}(x)=mex{\operatorname{sg}(y)\mid y$ 是 $x$ 的后继$}$，也就是上面所说的状态等级。&lt;/p&gt;
&lt;p&gt;对于一个ICG来说，状态 $x$ 是必败态，当且仅当 $\operatorname{sg}(x)=0$.&lt;/p&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;p&gt;两个人从一堆 $n$ 个物品中轮流取物品，每次取 $1, 3, 4$ 个物品，最后没法取的人输。&lt;/p&gt;
&lt;p&gt;$\operatorname{sg}(0)=mex(\emptyset)$，&lt;/p&gt;
&lt;p&gt;$\operatorname{sg}(1)=mex{\operatorname{sg}(0)}=mex{0}=1$，&lt;/p&gt;
&lt;p&gt;$\operatorname{sg}(5)=mex{\operatorname{sg}(4), \operatorname{sg}(2), \operatorname{sg}(1)}=3$.&lt;/p&gt;
&lt;h1&gt;P1247 取火柴游戏&lt;/h1&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;给定数列 $n_1, n_2, \cdots, n_k$，表示有 $k$ 堆物品，第 $i$ 堆物品的数量为 $n_i$。&lt;/p&gt;
&lt;p&gt;两人轮流从中取物品，规则：&lt;strong&gt;每次可以从一堆中拿走任意正整数个物品&lt;/strong&gt;。先拿走最后一根的人胜利。&lt;/p&gt;
&lt;p&gt;对于给定的数列，判断先手是否必胜，若必胜，输出第一次应该在哪堆取多少物品。&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;h3&gt;必胜必败分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;先手必胜，当且仅当 $n_1\oplus n_2\oplus\cdots\oplus n_k\neq 0$。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于其他 &lt;code&gt;nim游戏&lt;/code&gt; ，假设状态表示为 $(a_1, a_2, \cdots,a_n)$，那么先手必胜，当且仅当 $\operatorname{sg}(a_1)\oplus \operatorname{sg}(a_2)\oplus\cdots\oplus \operatorname{sg}(a_n)$，证明BFS。&lt;/p&gt;
&lt;p&gt;本题中 $\operatorname{sg}(i)=i$，所以有上面的结论。&lt;/p&gt;
&lt;h3&gt;证明&lt;/h3&gt;
&lt;p&gt;设 $s=n_1\oplus n_2\oplus\cdots\oplus n_k$.&lt;/p&gt;
&lt;p&gt;也就是证明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 $s \neq 0$ 时，存在一种取法，使得 $s=0$.&lt;/li&gt;
&lt;li&gt;当 $s=0$ 时，无论怎么取物品，$s$ 都不等于 $0$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;命题一&lt;/h4&gt;
&lt;p&gt;因为 $s\neq 0$，根据异或的定义，存在一堆物品 $i$，满足 $n_i\oplus s&amp;#x3C;n_i$ ，那么就从第 $i$ 堆取走 $n_i-(n_i\oplus s)$，剩下 $n_i\oplus s$ 个物品。&lt;/p&gt;
&lt;p&gt;此时，$s_{\rm{new}}=n_1\oplus n_2\cdots\oplus n_k\oplus s=s\oplus s=0$.&lt;/p&gt;
&lt;p&gt;所以命题一成立。&lt;/p&gt;
&lt;h4&gt;命题二&lt;/h4&gt;
&lt;p&gt;反证法。若否，则 $s=0$ 时，存在一种取物品的方法使得 $s_{\rm{new}}=0$.&lt;/p&gt;
&lt;p&gt;设取走第 $i$ 堆的若干物品，第 $i$ 堆剩余 $n_i&apos;$ 个物品。&lt;/p&gt;
&lt;p&gt;所以 $s_{\rm{new}}=n_1\oplus n_2\cdots\oplus n_i&apos;\oplus\cdots\oplus n_k=0=s$.&lt;/p&gt;
&lt;p&gt;把 $s$ 的定义式代入，得到 $n_i=n_i&apos;$，产生矛盾。&lt;/p&gt;
&lt;p&gt;所以命题二成立。&lt;/p&gt;
&lt;h3&gt;找方案的方法&lt;/h3&gt;
&lt;p&gt;利用命题一。找到一堆 $i$，满足 $n_i\oplus s&amp;#x3C;n_i$，将这一堆拿走 $n_i-(n_i\oplus s)$ 个物品，使得新的异或和为 $0$，满足题意。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;
const int MAXN = 1000008;
int T, n;
int a[MAXN];
int main() {
    scanf(&quot;%d&quot;, &amp;#x26;n);
    int res = 0;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        scanf(&quot;%d&quot;, &amp;#x26;a[i]);
        res ^= a[i];
    }
    if(res == 0) {
        cout &amp;#x3C;&amp;#x3C; &quot;lose&quot; &amp;#x3C;&amp;#x3C; endl;
    } else {
    	for(int i = 1; i &amp;#x3C;= n; i++) {
    		if((res ^ a[i]) &amp;#x3C; a[i]) {
    			cout &amp;#x3C;&amp;#x3C; a[i] - (res ^ a[i]) &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; endl;
    			a[i] = res ^ a[i];
    			break;
    		}
    	}
    	for(int i = 1; i &amp;#x3C;= n; i++) {
    		cout &amp;#x3C;&amp;#x3C; a[i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
    	}
    	cout &amp;#x3C;&amp;#x3C; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;P2148 [SDOI2009]E&amp;#x26;D&lt;/h1&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;有 $2n$ 堆石子，编号分别为 $1, 2, \cdots, 2n$。将第 $2k-1$ 和 $2k$ 堆 $(1\leq k\leq n)$，归为同一组。第 $i$ 堆石子的个数是正整数 $s_i$.&lt;/p&gt;
&lt;p&gt;定义分割操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任取一堆石子，不妨为第 $i$ 堆，将其全移走；&lt;/li&gt;
&lt;li&gt;从和第 $i$ 堆同一组的石子中，取出若干正整数个石子，放到第 $i$ 堆的位置上。操作后，两堆石子数都要大于 $0$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;给定状态，问先手是否有必胜策略。&lt;/p&gt;
&lt;p&gt;$n\leq 2\times 10^4, 1\leq s_i\leq 2\times 10^9$.&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;$f(x)$ 为 $x$ 的二进制末尾首个 $0$ 的出现位置（标号从 $0$ 开始）。比如 $f(5)=(101)_2=1$.&lt;/p&gt;
&lt;p&gt;$\operatorname{sg}(x, y)$ 为一组分别有 $x+1, y+1$ 个石子的 $\operatorname{sg}$ 值。&lt;/p&gt;
&lt;p&gt;$S_z$ 表示满足 $x+y+1=z$ 的 $\operatorname{sg}(x, y)$ 构成的自然数集合。$s_0=\emptyset$.&lt;/p&gt;
&lt;h3&gt;引理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;终止局面的 $\operatorname{sg}$ 值为 $0$，即 $\operatorname{sg}(0, 0)=0$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\operatorname{sg}(x, y)=mex($ 所有后继的 $\operatorname{sg}$ 值 $)=mex(S_x\cup s_y)$.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;$S_z$ 等同于 $z$ 二进制下 $1$ 的位置集合。比如 $s_5=(101)_2={0, 2}$.&lt;/li&gt;
&lt;li&gt;$\operatorname{sg}(x, y)=f(x\mid y)$. 比如 $sg(1, 4)=f(5)=1$.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;证明&lt;/h3&gt;
&lt;p&gt;考虑归纳证明。逐步放大 $z$，证明对于任意的 $z$，结论 $1$ 成立，且对于任意的 $x+y=z$，结论 $2$ 成立。&lt;/p&gt;
&lt;p&gt;根据引理 $1$，结论 $2$ 在 $z=0$ 时正确。&lt;/p&gt;
&lt;p&gt;假设已经证明结论 $2$ 在 $z-1$ 正确，考虑 $S_z$ 包含元素 $p$ 的条件。&lt;/p&gt;
&lt;p&gt;由 $S_z$ 的定义，需要 $x+y=z-1$，由结论 $2$，需要 $f(x\mid y)=p$.&lt;/p&gt;
&lt;p&gt;因此，$(x\mid y)$ 的二进制第 $p$ 位是 $0$，$0\sim p-1$ 位都是 $1$. 所以 $x+y$ 的 $0\sim p-1$ 位都是 $1$，第 $p$ 位是 $0$.&lt;/p&gt;
&lt;p&gt;又因为 $x+y+1=z$，所以当包含元素 $p$ 时，无论如何，二进制下 $z$ 的第 $p$ 位总为 $1$.&lt;/p&gt;
&lt;p&gt;所以，当且仅当 $z$ 的二进制第 $p$ 位为 $1$ 时，$S_z$ 包含元素 $p$，结论 $1$ 得证。&lt;/p&gt;
&lt;p&gt;因为结论 $1$ 成立，所以 $\operatorname{sg}(x, y)=mex(S_x\cup S_y)=mex(S_{x\mid y})$.&lt;/p&gt;
&lt;p&gt;又因为 $x\mid y\leq x+y=z$ 在已经证明的 $z$ 的范围内，根据 $S$ 和 $f$ 的定义，显然 $mex(S_{x\mid y})=f(x\mid y)$.&lt;/p&gt;
&lt;p&gt;故结论 $1, 2$ 得证。&lt;/p&gt;
&lt;h3&gt;解法&lt;/h3&gt;
&lt;p&gt;根据上述结论，$\operatorname{sg}(x, y)=f(x\mid y)$，可以快速地求出每组的 $sg$ 值。将所有组的 $\operatorname{sg}$ 值异或起来，即可判断是否必胜。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;

using namespace std;
int T, n;
int lowzero(int x){
    for (int i = 0;; i++, x &gt;&gt;= 1) {
        if (!(x &amp;#x26; 1)) return i;
    }
}
int main() {
	scanf(&quot;%d&quot;, &amp;#x26;T);
	while(T--) {
        int ans = 0;
        scanf(&quot;%d&quot;, &amp;#x26;n); n &gt;&gt;= 1;
        while(n--) {
            int x, y;
            scanf(&quot;%d%d&quot;, &amp;#x26;x, &amp;#x26;y);
            ans ^= lowzero((x - 1) | (y - 1));
        }
        if(ans) printf(&quot;YES\n&quot;);
        else printf(&quot;NO\n&quot;);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;p&gt;威佐夫博弈：https://blog.csdn.net/wu_tongtong/article/details/79295069&lt;/p&gt;
&lt;p&gt;关于异或：https://blog.csdn.net/hndu__lz/article/details/61925818&lt;/p&gt;
&lt;p&gt;P2148：https://www.luogu.com.cn/blog/Sooke/solution-p2148&lt;/p&gt;</content:encoded></item><item><title>【ZROI-19普转提-Day5-T3】打游戏</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-19-day5-t3</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-19-day5-t3</guid><description>算法题解</description><pubDate>Sun, 03 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;【ZROI-19普转提-Day5-T3】 打游戏&lt;/h1&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;原题面地址：&lt;a href=&quot;http://zhengruioi.com/contest/400/problem/1003&quot;&gt;【普转提七联测 Day 2】打游戏 - Zhengrui Online Judge&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{1}$&lt;/h3&gt;
&lt;p&gt;对于 $20%$ 的数据，魔法值为 $0$。&lt;/p&gt;
&lt;p&gt;不能放技能，容易想到贪心：按照生命值升序排序，从左到右逐个击破。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n)$.&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $2$&lt;/h3&gt;
&lt;p&gt;对于另 $10%$ 的数据，魔法值为 $1$.&lt;/p&gt;
&lt;p&gt;只能放一次技能。显然在第一回合中先放技能是最优的。&lt;/p&gt;
&lt;p&gt;于是可以确定第一回合放技能，后面的所有回合按照 $\rm{subtask}$ $1$ 中的贪心策略模拟。&lt;/p&gt;
&lt;p&gt;只需要枚举一下第一回合放哪个技能即可。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n)$.&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{3}$&lt;/h3&gt;
&lt;p&gt;对于另 $10%$ 的数据，$n\leq 1000$ 且魔法值 $\leq 18$.&lt;/p&gt;
&lt;p&gt;注意到 $m$ 很小。与 $\rm{subtask}$ $\rm{2}$ 相同，显然把这 $18$ 次技能放在前 $18$ 个回合是最优的。于是可以 $2^{18}$ 暴力枚举前 $18$ 个回合的出技能状态，后面的回合按照 $\rm{subtask}$ $\rm{1}$ 的贪心策略暴力模拟即可。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n+n\cdot 2^m)$.&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{4}$&lt;/h3&gt;
&lt;p&gt;对于另 $10%$ 的数据，$n\leq 10^5$ 且魔法值 $\leq 18$.&lt;/p&gt;
&lt;p&gt;与 $\rm{subtask}$ $\rm{3}$ 相比，$n$ 的值变大了。考虑用变量记录前 $18$ 回合中使用的群攻次数，重击直接在数组上修改。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n+2^m)$.&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{5}$&lt;/h3&gt;
&lt;p&gt;对于另 $30%$ 的数据，$n\leq 50$，$m\leq 100$.&lt;/p&gt;
&lt;p&gt;考虑动态规划。&lt;/p&gt;
&lt;p&gt;设 $\operatorname{DP}_{i, c, z, q}$ 表示当前有 $i$ 点魔法值，正在打第 $c$ 个怪物，当前用了 $z$ 次重击，$q$ 次群攻，从现在到结束的最少花费。从后往前转移。&lt;/p&gt;
&lt;p&gt;根据定义，要求的答案就是 $\operatorname{DP}_{m, 1, 0, 0}$ 的值。&lt;/p&gt;
&lt;p&gt;容易想到转移方法和初始化方法。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n+nm^2)$.&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{6}$&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$n\leq 10^5$，$m\leq 100$.&lt;/p&gt;
&lt;p&gt;注意到 $m$ 的值很小，但非常不幸，对想到正解没有什么正面的帮助。&lt;/p&gt;
&lt;p&gt;首先把部分分的贪心迁移过来。按照生命值升序一个一个打，并且要先把技能都用完。&lt;/p&gt;
&lt;p&gt;考虑是否存在更强的贪心策略。&lt;/p&gt;
&lt;p&gt;**如果魔法值没了：**按照生命值升序一个一个打。&lt;/p&gt;
&lt;p&gt;**如果还有魔法值：**思考一下可以得到：&lt;/p&gt;
&lt;p&gt;如果当前不止两个怪，出群攻一定不劣于出重击；&lt;/p&gt;
&lt;p&gt;如果当前只剩两个怪，从小到大重击。&lt;/p&gt;
&lt;p&gt;于是可以贪心地完成。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(n\log n)$，复杂度瓶颈竟然在排序上…&lt;/p&gt;</content:encoded></item><item><title>二分图 最大匹配 匈牙利算法</title><link>https://www.tonyyin0418.com/blog/oi-notes/bi-graph</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-notes/bi-graph</guid><description>二分图、最大匹配、匈牙利算法的一些学习笔记</description><pubDate>Sat, 02 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;二分图&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;h4&gt;二分图&lt;/h4&gt;
&lt;p&gt;对于一个无向图 $G=(V, E)$，可以将 $V$ 分为两个不相交的子集，使得任意一条边的&lt;strong&gt;顶点分属两个不同的点集&lt;/strong&gt;，那么这个无向图是二分图。&lt;/p&gt;
&lt;h4&gt;匹配&lt;/h4&gt;
&lt;p&gt;对于无向图 $G=(V, E)$，取边集的子集 $E^&lt;em&gt;$，使得 $E^&lt;/em&gt;$ 中&lt;strong&gt;任意两条边没有公共顶点&lt;/strong&gt;，那么 $E^*$ 是图 $G$ 的一个匹配，又称边独立集。&lt;/p&gt;
&lt;h4&gt;最大匹配&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;边数最多&lt;/strong&gt;的匹配。极大匹配的边数唯一，取法可能不唯一。&lt;/p&gt;
&lt;h4&gt;匹配边 &amp;#x26; 非匹配边&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在边独立集中&lt;/strong&gt;的边是匹配边，反之是非匹配边。&lt;/p&gt;
&lt;h4&gt;匹配点 &amp;#x26; 非匹配点&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;能被匹配边连接到&lt;/strong&gt;的点是匹配点，反之是非匹配点。&lt;/p&gt;
&lt;h4&gt;完美匹配&lt;/h4&gt;
&lt;p&gt;若 $M$ 是图 $G$ 的一个匹配，且 $G$ 中的&lt;strong&gt;每个顶点都是匹配点&lt;/strong&gt;，那么 $M$ 是 $G$ 的完美匹配。&lt;/p&gt;
&lt;h4&gt;交错路径&lt;/h4&gt;
&lt;p&gt;图 $G$ 中，由匹配边和非匹配边&lt;strong&gt;交替&lt;/strong&gt;构成的路径，是交错路径。&lt;/p&gt;
&lt;h4&gt;增广路径&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;起点和终点都是非匹配点的交错路径&lt;/strong&gt;，是增广路径。&lt;/p&gt;
&lt;p&gt;显然，$M$ 是 $G$ 的最大匹配，当且仅当 $G$ 中不含关于 $M$ 的增广路径。&lt;/p&gt;
&lt;p&gt;因为如果 $G$ 中含有关于 $M$ 的增广路径，把增广路径上的匹配边取反，可以使匹配大小增加$1$.&lt;/p&gt;
&lt;h2&gt;二分图最大匹配&lt;/h2&gt;
&lt;h4&gt;概述&lt;/h4&gt;
&lt;p&gt;在二分图中找到一个最大匹配。&lt;/p&gt;
&lt;h4&gt;Hall定理&lt;/h4&gt;
&lt;p&gt;二分图 $G=&amp;#x3C;V_1, V_2, E&gt;$，其中 $|V_1| \leq |V_2|$，则 $G$ 中存在  $V_1$ 到 $V_2$ 的完美匹配，当且仅当 $V_1$ 中任意 $k\in [1, |V_1|]$ 个顶点，都至少与 $V_2$ 中的 $k$ 个顶点相连。&lt;/p&gt;
&lt;p&gt;反证即可。&lt;/p&gt;
&lt;h4&gt;思想&lt;/h4&gt;
&lt;p&gt;对一条增广路进行异或之后，能使匹配中的边数增加 $1$.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.jisuanke.com/img/upload/20180723/9d66ed94c7de4e0a428e49185b3e340072fb2e85.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;异或之后，可以得到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.jisuanke.com/img/upload/20180723/5ea44d9cc73bce365f3d61ee6040bdd2fe763116.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;匈牙利算法&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;将初始的匹配 $M$ 置为 $\emptyset$.&lt;/li&gt;
&lt;li&gt;找到一条增广路径，通过异或操作更新最大匹配。&lt;/li&gt;
&lt;li&gt;重复2.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：$\mathcal{O}(EV)$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码&lt;/strong&gt;（From Jisuanke）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;const int MAX_N = 100;
const int MAX_M = 10000;
struct Edge {
    int v, next;
} e[MAX_M];
int p[MAX_N], eid;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    e[eid].v = v;
    e[eid].next = p[u];
    p[u] = eid++;
}
bool vst[MAX_N];
int rmatch[MAX_N];
bool dfs(int u) {
    for(int i = p[u]; i != -1; i = e[i].next) {
        int v = e[i].v;
        if(!vst[v]) {
            vst[v] = true;
        	if(rmatch[v] == -1 || dfs(rmatch[v])) {
                rmatch[v] = u;
                return true;
            }
        }
    }
    return false;
}
int hungary(int n) {
    int cnt = 0;
    memset(rmatch, -1, sizeof(rmatch));
    for(int i = 1; i &amp;#x3C;= n; i++) {
        memset(vst, 0, sizeof(vst));
        cnt += dfs(i);
    }
    return cnt;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【AtCoder-4540】Dight Sum</title><link>https://www.tonyyin0418.com/blog/oi-solution/at4540</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/at4540</guid><description>算法题解</description><pubDate>Wed, 30 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;更好的阅读体验：&lt;a href=&quot;https://www.tonyyin.top/2020/12/at4540/&quot;&gt;TonyYin&apos;s Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/AT4540&quot;&gt;AT4540 Digit Sum - 洛谷 _ 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;求区间 $[1, k]$ 中有多少个整数满足：其十进制表示的数字和为 $D$ 的倍数。答案对 $1e9+7$ 取模。&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;p&gt;观察数据范围：$1\leq k\leq 10^{10000}$.&lt;/p&gt;
&lt;p&gt;再看题面，形如”&lt;strong&gt;求在数位限制下有多少满足条件的数&lt;/strong&gt;“，想到数位 $\rm{DP}$.&lt;/p&gt;
&lt;p&gt;如果你没有学过数位 $\rm{DP}$，那么可以参考&lt;a href=&quot;https://blog.csdn.net/KonnyWen/article/details/104475276&quot;&gt;这篇博客&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果你学过数位 $\rm{DP}$，那么现在考虑状态如何设置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态设置：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先可以看出，这道题不需要前导零标记。&lt;/p&gt;
&lt;p&gt;第二步需要思考 $\rm{DP}$ 中需要存储什么前缀状态。通过题目描述可以比较容易的看出，我们只需要存储前缀数字和对 $D$ 取模的余数即可，将其记为 $\rm{pre}$，&lt;/p&gt;
&lt;p&gt;那么能对答案作出贡献的条件就是：前缀数字和 $\bmod D =0$，也就是 $pre = 0$.&lt;/p&gt;
&lt;p&gt;至此，根据数位 $\rm{DP}$ 的套路，我们可以设置如下状态：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$\rm{DP}[pos][pre][sta]$ 表示还有 $pos$ 位需要处理；&lt;/p&gt;
&lt;p&gt;当前状态的前缀数字和 $\bmod D$ 是 $pre$；&lt;/p&gt;
&lt;p&gt;前缀的最大值状态是 $sta$，若前 $len-pos$ 位与题目给出的 $k$ 相同，则 $sta=0$，反之亦然。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;状态转移：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是可以使用记忆化搜索来实现数位 $\rm{DP}$，详见代码注释。&lt;/p&gt;
&lt;p&gt;个人觉得注释还比较清晰，认真多看几遍是能看懂的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int dfs(int pos, int res, int sta) {
	if(pos == 0) return res == 0; //能对答案作出贡献的条件：前缀数字和 mod D == 0
	if(dp[pos][res][sta] != -1) return dp[pos][res][sta]; //记忆化搜索
	int ret = 0, maxx = 9; //ret是函数返回值，maxx是当前位可选的最大值
    if(sta) maxx = num[pos]; //如果当前前缀与k的前缀相同，那么这一位最大值就是num[pos]
	for(int i = 0; i &amp;#x3C;= maxx; i++) { //之后遍历这一位可选的数字
		ret += dfs(pos - 1, (res + i) % d, sta &amp;#x26;&amp;#x26; (i == maxx));
        //pos - 1: 继续看下一位
        //(res + i) % d: 把前缀数字和加上新选的这一位
        //sta &amp;#x26;&amp;#x26; (i == maxx): 前缀and当前这一位, 都与k的前缀相同，那么新的前缀还是最大值状态 
	}
	dp[pos][res][sta] = ret;//记忆化
	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于加上取模之后不美观，上面的代码没有给 $ret$ 加取模，需要加上。（下面的代码中是有取模的）&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;p&gt;放出完整代码，可以再重点看一下 &lt;code&gt;solve()&lt;/code&gt; 函数里面的初始化。&lt;/p&gt;
&lt;p&gt;关键部分的注释已经在上面了。&lt;/p&gt;
&lt;p&gt;如果没有看懂，建议再读几遍&lt;a href=&quot;https://blog.csdn.net/KonnyWen/article/details/104475276&quot;&gt;这篇博客&lt;/a&gt;，或者自行百度学习数位 $\rm{DP}$.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
using namespace std;
const int MAXLEN = 10008;
const int MAXD = 108;
const int mod = 1e9 + 7;
int d, num[MAXLEN];
string s;
int dp[MAXLEN][MAXD][2];
int dfs(int pos, int res, int sta) {
	if(pos == 0) return (res == 0);
	if(dp[pos][res][sta] != -1) return dp[pos][res][sta];
	int ret = 0, maxx = 9;
	if(sta) maxx = num[pos];
	for(int i = 0; i &amp;#x3C;= maxx; i++) {
		ret = (ret + dfs(pos - 1, (res + i) % d, sta &amp;#x26;&amp;#x26; (i == maxx))) % mod;
	}
	dp[pos][res][sta] = ret;
	return ret;
}
int solve() {
	memset(dp, -1, sizeof(dp));
	for(int i = 0; i &amp;#x3C; s.length(); i++) {
		num[i + 1] = s[s.length() - i - 1] - &apos;0&apos;;
	}
	return dfs(s.length(), 0, 1);
}
signed main() {
	cin &gt;&gt; s;
	scanf(&quot;%lld&quot;, &amp;#x26;d);
	printf(&quot;%lld\n&quot;, ((solve() - 1) % mod + mod) % mod);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P1962】斐波那契数列</title><link>https://www.tonyyin0418.com/blog/oi-solution/p1962</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p1962</guid><description>矩阵快速幂，求斐波那契数列第 n 项。</description><pubDate>Wed, 30 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/P1962&quot;&gt;P1962 斐波那契数列 - 洛谷 | 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前置知识点&lt;/h2&gt;
&lt;h3&gt;矩阵&lt;/h3&gt;
&lt;h4&gt;定义：&lt;/h4&gt;
&lt;p&gt;一个$n\times m$矩阵是由$n\times m$个实数排列成$n$行$m$列构成的，用一对[]括起来。&lt;/p&gt;
&lt;p&gt;下面$3\times 3$矩阵简记为$A=(a_{ij})&lt;em&gt;{3\times 3}$
$$
A=\left[
\begin{matrix}
a&lt;/em&gt;{11} &amp;#x26; a_{12} &amp;#x26; a_{13}\
a_{21} &amp;#x26; a_{22} &amp;#x26; a_{23}\
a_{31} &amp;#x26; a_{32} &amp;#x26; a_{33}\
\end{matrix}
\right]
$$&lt;/p&gt;
&lt;h4&gt;矩阵加减&lt;/h4&gt;
&lt;p&gt;$n$行$m$列矩阵$A,B$加减规则：$A\pm B=(a_{ij}\pm b_{ij})_{n\times m}$&lt;/p&gt;
&lt;p&gt;矩阵加减满足交换律，结合律
$$
\begin{aligned} A\pm B&amp;#x26;=
\left[\begin{matrix}
a_{11}&amp;#x26;a_{12}&amp;#x26;\cdots&amp;#x26;a_{1m}\
a_{21}&amp;#x26;a_{22}&amp;#x26;\cdots&amp;#x26;a_{2m}\
\cdots&amp;#x26;\cdots&amp;#x26;\cdots&amp;#x26;\cdots\
a_{n1}&amp;#x26;a_{n2}&amp;#x26;\cdots&amp;#x26;a_{nm}\
\end{matrix}\right]\pm
\left[\begin{matrix}
b_{11}&amp;#x26;b_{12}&amp;#x26;\cdots&amp;#x26;b_{1m}\
b_{21}&amp;#x26;b_{22}&amp;#x26;\cdots&amp;#x26;b_{2m}\
\cdots&amp;#x26;\cdots&amp;#x26;\cdots&amp;#x26;\cdots\
b_{n1}&amp;#x26;b_{n2}&amp;#x26;\cdots&amp;#x26;b_{nm}\
\end{matrix}\right]\&amp;#x26;=
\left[\begin{matrix}
a_{11}\pm b_{11}&amp;#x26;a_{12}\pm b_{12}&amp;#x26;\cdots&amp;#x26;a_{1m}\pm b_{1m}\
a_{21}\pm b_{21}&amp;#x26;a_{22}\pm b_{22}&amp;#x26;\cdots&amp;#x26;a_{2m}\pm b_{2m}\
\cdots&amp;#x26;\cdots&amp;#x26;\cdots&amp;#x26;\cdots\
a_{n1}\pm b_{n1}&amp;#x26;a_{n2}\pm b_{n2}&amp;#x26;\cdots&amp;#x26;a_{nm}\pm b_{nm}\
\end{matrix}\right]
\end{aligned}
$$&lt;/p&gt;
&lt;h4&gt;矩阵乘法&lt;/h4&gt;
&lt;p&gt;**1.数和矩阵相乘&lt;/p&gt;
&lt;p&gt;$$
kA=Ak=
\left[\begin{matrix}
ka_{11}&amp;#x26;ka_{12}&amp;#x26;\cdots&amp;#x26;ka{1m}\
ka_{21}&amp;#x26;ka_{22}&amp;#x26;\cdots&amp;#x26;ka{2m}\
\cdots&amp;#x26;\cdots&amp;#x26;\cdots&amp;#x26;\cdots\
ka_{n1}&amp;#x26;ka_{n2}&amp;#x26;\cdots&amp;#x26;ka{nm}\
\end{matrix}\right]
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.矩阵和矩阵相乘&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原理：$C_{ij}=A$的第$i$行和$B$的第$j$列每个元素对应相乘的和。&lt;/p&gt;
&lt;p&gt;所以矩阵乘法当且仅当A的列数=B的行数时才有定义。&lt;/p&gt;
&lt;p&gt;由于矩阵乘法的规则，$A\cdot B\not= B\cdot A$&lt;/p&gt;
&lt;p&gt;具体方法：$\textcolor{red}{C_{ij}}=a_{i1}b_{1j}+a_{i2}b_{2j}+\cdots+a_{it}b_{tj}=\textcolor{red}{\sum_{r=1}^{t}a_{ir}b_{rj}}$&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct matrix {
   int n, m;
   int a[100][100];
};
// A.m == B.n
matrix matrix_mul(matrix A, matrix B) {
    matrix C;
    C.n = A.n;
    C.m = B.m;
    for (int i = 0; i &amp;#x3C; A.n; ++i) {
        for (int j = 0; j &amp;#x3C; B.m; ++j) {
            C.a[i][j] = 0;
            for (int k = 0; k &amp;#x3C; A.m; ++k) {
                C.a[i][j] += A.a[i][k] * B.a[k][j];
            }
        }
    }
    return C;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单位矩阵&lt;/h3&gt;
&lt;p&gt;形如：
$$
\left[\begin{matrix}
1&amp;#x26;0&amp;#x26;\cdots&amp;#x26;0\
0&amp;#x26;1&amp;#x26;\cdots&amp;#x26;0\
\cdots&amp;#x26;\cdots&amp;#x26;\cdots&amp;#x26;\cdots\
0&amp;#x26;0&amp;#x26;0&amp;#x26;1\
\end{matrix}\right]
$$
主对角线上元素均为1，其余元素均为0的矩阵。通常记做$I_n$或$E_n$&lt;/p&gt;
&lt;p&gt;性质：&lt;/p&gt;
&lt;p&gt;$A\cdot I_n=A$&lt;/p&gt;
&lt;p&gt;$I_n\cdot A=A$&lt;/p&gt;
&lt;p&gt;即任何一个矩阵左乘或右乘单位矩阵，结果不变（相当于乘法当中的 $1$ ）&lt;/p&gt;
&lt;h2&gt;矩阵快速幂&lt;/h2&gt;
&lt;h3&gt;快速幂&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int quick_power(int x,int y){
    int ans = 1,cnt = x;
    while(y){
        if(y &amp;#x26; 1){
           ans *= cnt;
        }
        cnt *= cnt;
        y &gt;&gt;= 1;
    }
   return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;矩阵快速幂&lt;/h3&gt;
&lt;p&gt;利用快速幂思想，快速求矩阵$A^n$&lt;/p&gt;
&lt;p&gt;将快速幂中的乘法替换为矩阵乘法，参数和返回值替换为矩阵即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int n; // 所有矩阵都是 n * n 的矩阵
struct matrix {
   int a[100][100];
};
matrix matrix_mul(matrix A, matrix B, int mod) {
    // 2 个矩阵相乘
    matrix C;
    for (int i = 0; i &amp;#x3C; n; i++) {
        for (int j = 0; j &amp;#x3C; n; j++) {
            C.a[i][j] = 0;
            for (int k = 0; k &amp;#x3C; n; ++k) {
                C.a[i][j] = (C.a[i][j] + A.a[i][k] * B.a[k][j] % mod) % mod;
            }
        }
    }
    return C;
}
matrix unit() {
    // 返回一个单位矩阵
    matrix res;
    for (int i = 0; i &amp;#x3C; n; i++) {
        for (int j = 0; j &amp;#x3C; n; j++) {
            if (i == j) {
                res.a[i][j] = 1;
            } else {
                res.a[i][j] = 0;
            }
        }
    }
    return res;
}
matrix matrix_pow(matrix A, int p, int mod) {
    // 快速求矩阵 A 的 p 次方
    matrix res = unit();
    while (p &gt; 0) {
        if (p &amp;#x26; 1) {
            res = matrix_mul(res, A, mod);
        }
        A = matrix_mul(A, A, mod);
        p &gt;&gt;= 1;
    }
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;本题题解&lt;/h2&gt;
&lt;p&gt;斐波那契数列递推式：
$$
a_n=a_{n-1}+a_{n-2}
$$
设$A_i=\left[\begin{matrix}a_i,a_{i+1}\end{matrix}\right]$&lt;/p&gt;
&lt;p&gt;先构造$1\times 2$的矩阵$A_1=\left[\begin{matrix}a_1,a_2\end{matrix}\right]$，考虑构造矩阵 $B$ 使得 $A_1\times B=A_2$&lt;/p&gt;
&lt;p&gt;通过观察推导得到$B=\left[\begin{matrix}0&amp;#x26;1\1&amp;#x26;1\end{matrix}\right]$
$$
\begin{aligned}
&amp;#x26;\therefore A_2=A_1\times B\
&amp;#x26;\therefore A_3=A_2\times B=A_1\times B\times B=A_1\times B^2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;$$
\cdots
$$&lt;/p&gt;
&lt;p&gt;$$
A_i=A_1\times B^{i-1}=
\left[\begin{matrix}1,1\end{matrix}\right]
\times
\left[\begin{matrix}
0&amp;#x26;1\
1&amp;#x26;1\
\end{matrix}\right]^{i-1}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：$\mathcal{O}(\log N)$&lt;/p&gt;</content:encoded></item><item><title>【AtCoder-4846】Max-Min Sums</title><link>https://www.tonyyin0418.com/blog/oi-solution/at4846</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/at4846</guid><description>算法题解</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;更好的阅读体验：&lt;a href=&quot;https://www.tonyyin.top/2020/12/at4846/&quot;&gt;TonyYin&apos;s Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/AT4846/&quot;&gt;AT4846 [ABC151E] Max-Min Sums - 洛谷 _ 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;给定 $n$ 个数，构成集合 $A$。从 $A$ 中任取 $k$ 个数，构成 $A$ 的子集 $S$.&lt;/p&gt;
&lt;p&gt;记 $\rm{Max}$ 为 $S$ 中的最大值，$\rm{Min}$ 为 $S$ 中的最小值，$f(S)=\rm{Max-Min}$.&lt;/p&gt;
&lt;p&gt;题目要求：所有满足 $|S|=k$ 的 $f(S)$ 之和。（对 $1e9+7$ 取模）&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;p&gt;观察数据范围，期望时间复杂度为： $\mathcal{O}(n\log n)$ 或者 $\mathcal{O}(n\log |x|)$ 的做法。&lt;/p&gt;
&lt;p&gt;暴力的方法就是 $C_n^k$ 枚举元素个数为 $k$ 的子集，然后找最大值和最小值，复杂度爆炸，期望得分 $0$ 分.&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;考虑如何&lt;strong&gt;统计作为子集中最大值的次数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体地，对于前 $k - 1$ 个数，不可能成为任何一个子集的最大值；对于其余的数，第 $i$ 个数成为最大值的方法数是 $C_{i-1}^{k-1}$ 种，也就是在前 $i-1$ 个数中取 $k-1$ 个，与第 $i$ 个数一起构成子集，这样的子集能使 $i$ 为最大值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = k; i &amp;#x3C;= n; i++) { //算一遍最大值
    ans += a[i] * C(i - 1, k - 1);
}
//为了美观，这里的代码没有取模。下面Code部分的代码有取模。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;strong&gt;统计作为子集中最小值的次数&lt;/strong&gt;，与最大值同理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n - k + 1; i++) { //算一遍最小值
    ans -= a[i] * C(n - i, k - 1);
}
//为了美观，这里的代码没有取模。下面Code部分的代码有取模。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个数据范围下，&lt;strong&gt;使用逆元求组合数&lt;/strong&gt;就可以了，如果不会用逆元求组合数，可以看一下我写的&lt;a href=&quot;https://tonyyin.blog.luogu.org/jin-jie-shuo-lun&quot;&gt;这篇博客&lt;/a&gt;，往下翻，可以找到关于组合数取模的部分。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;p&gt;给出完整的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define MAXN 100008
using namespace std;
int mod = 1e9 + 7;
int n, k, a[MAXN], fac[MAXN], ans = 0;
int power(int x, int k, int p) {
    int ret = 1;
    while(k) {
        if(k &amp;#x26; 1) ret = ret * x % p;
        k &gt;&gt;= 1;
        x = x * x % p;
    }
    return ret % p;
}
int inv(int x, int p) {//求x关于模p的逆元
    return power(x, p - 2, p) % mod;
}
int C(int a, int b) {//直接用逆元求解组合数C(a, b)
    return fac[a] * inv(fac[b], mod) % mod * inv(fac[a - b], mod) % mod;
}
signed main() {
    scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;k);
    for(int i = 1; i &amp;#x3C;= n; i++) scanf(&quot;%lld&quot;, &amp;#x26;a[i]);
    fac[0] = 1;
    for(int i = 1; i &amp;#x3C;= n; i++) fac[i] = fac[i - 1] * i % mod;
    sort(a + 1, a + n + 1); //升序排序
    //对于每个数，计算在多少种个子集中，这个数能作为最大值/最小值
    //作为最大值的可能情况是C(i - 1, k - 1)，最小值类似
    for(int i = k; i &amp;#x3C;= n; i++) { //算一遍最大值
        ans = (ans + a[i] * C(i - 1, k - 1) % mod) % mod;
    }
    for(int i = 1; i &amp;#x3C;= n - k + 1; i++) { //算一遍最小值
        ans = (ans - a[i] * C(n - i, k - 1) % mod) % mod;
    }
    cout &amp;#x3C;&amp;#x3C; (ans % mod + mod) % mod &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【AtCoder-2394】井井井</title><link>https://www.tonyyin0418.com/blog/oi-solution/at2394</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/at2394</guid><description>算法题解</description><pubDate>Sun, 27 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;更好的阅读体验：&lt;a href=&quot;https://www.tonyyin.top/2020/12/at2394/&quot;&gt;TonyYin - Blog&lt;/a&gt;
题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/AT2394&quot;&gt;AT2394 [ARC071B] 井井井 / ### – 洛谷 | 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;$\rm{Description}$&lt;/h2&gt;
&lt;p&gt;在平面直角坐标系中，给定 $n$ 条平行于 $y$ 轴的线和 $m$ 条平行于 $x$ 轴的线，求由这些线组成的矩形的面积和。&lt;/p&gt;
&lt;h2&gt;$\rm{Solution}$&lt;/h2&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{1}$&lt;/h3&gt;
&lt;p&gt;考虑暴力做法，枚举矩形的左上角和右下角，也就是求：&lt;/p&gt;
&lt;p&gt;$$
\sum_{1\leq i\leq j\leq n}\sum_{1\leq k\leq l\leq m}(x_j-x_i)\cdot(y_l-y_k)&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;时间复杂度是 $\mathcal{O}(n^2m^2)$ .&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{2}$&lt;/h3&gt;
&lt;p&gt;对于任意一个矩形，想要确定它，只需要固定一条平行于 $x$ 轴的线段和一条平行于 $y$ 轴的线段。&lt;/p&gt;
&lt;p&gt;所以最终的答案可以写成：&lt;/p&gt;
&lt;p&gt;$$
\sum_{i\leq i\leq j\leq n}(x_j-x_i)\sum_{1\leq k\leq l\leq m}(y_l-y_k)&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;记 $A=\sum\limits_{1\leq i\leq j\leq n}(x_j-x_i)$，$B=\sum\limits_{1\leq k\leq l\leq m}(y_l-y_k)$，那么我们可以分开计算 $A$ 和 $B$。也就是分别计算：所有&lt;strong&gt;平行于 $x$ 轴的线段长度和&lt;/strong&gt;，以及所有&lt;strong&gt;平行于 $y$ 轴的线段长度和&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这样可以得到 $\mathcal{O}(n^2)$ 级别的算法。&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{3}$&lt;/h3&gt;
&lt;h4&gt;思路：&lt;/h4&gt;
&lt;p&gt;对于满分做法，我们期望 $\mathcal{O}(n)$ 的算法，现在想要进一步简化上面的式子。&lt;/p&gt;
&lt;p&gt;考虑如何快速求出所有&lt;strong&gt;平行于 $x$ 轴的线段长度和&lt;/strong&gt;。容易想到，我们只需要统计每条小线段被算了几次。显然对于线段 $(x_{i-1}, x_{i})$，被计算了 $(i - 1) \times(n-i-1)$ 次，于是我们就有了线性的做法。&lt;/p&gt;
&lt;p&gt;对于平行于 $y$ 轴的线段，计算方法完全相同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果到此为止您看懂了，那可以直接看代码了，如果没看懂，请看下面这部分。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;具体地：&lt;/h4&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;img src=&quot;https://cdn.tonyyin.top/2021/10/04/b82a596dfc279.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;对于这张图，线段长度和为 $|AB|+|AC|+|AD|+|BC|+|BD|+|CD|$.&lt;/p&gt;
&lt;p&gt;那么我们将上图中每个线段表示成&lt;strong&gt;基本线段的和&lt;/strong&gt;的形式，比如 $|AC|=|AB|+|BC|$，整理之后即：&lt;/p&gt;
&lt;p&gt;$$
3|AB|+4|BC|+3|CD|&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;现在考虑如何计算每条基本线段的贡献。&lt;/p&gt;
&lt;p&gt;根据小学数学知识，线段 $(x_i, x_{i - 1})$ 被算的次数是 $(i-1)\times (n - i - 1)$ ，这样我们就能 $\mathcal{O}(n)$ 地算出平行于 $x$ 轴的线段长度和。&lt;/p&gt;
&lt;p&gt;对于平行于 $y$ 轴的，用同样的方法计算。&lt;/p&gt;
&lt;h2&gt;$\rm{Code}$&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define MAXN 100008
using namespace std;
int n, m, x[MAXN], y[MAXN];
int A, B; //A为所有平行于 x 轴的线段长度和，B为平行于 y 轴的线段长度和
const int mod = 1e9 + 7;
signed main() {
    scanf(&quot;%lld%lld&quot;, &amp;#x26;n, &amp;#x26;m);
    for(int i = 1; i &amp;#x3C;= n; i++) scanf(&quot;%lld&quot;, &amp;#x26;x[i]);
    for(int i = 1; i &amp;#x3C;= m; i++) scanf(&quot;%lld&quot;, &amp;#x26;y[i]);
    sort(x + 1, x + n + 1); //横纵坐标升序排序
    sort(y + 1, y + m + 1);
    for(int i = 2; i &amp;#x3C;= n; i++) { //计算与x轴平行的所有线段的长度和
        A = (A + (x[i] - x[i - 1]) * (i - 1) * (n - i + 1)) % mod;
    }
    for(int i = 2; i &amp;#x3C;= m; i++) {
        B = (B + (y[i] - y[i - 1]) * (i - 1) * (m - i + 1)) % mod;
    }
    printf(&quot;%lld\n&quot;, A * B % mod);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【ZROI-19普转提-Day5-T3】背包</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-19pzt-day5-t3</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-19pzt-day5-t3</guid><description>算法题解</description><pubDate>Fri, 25 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;有 $n$ 个物品，编号为 $1$ ~ $n$，每个物品有重量 $w_i$，价值 $v_i$.&lt;/p&gt;
&lt;p&gt;有 $m$ 个背包，编号为 $1$ ~ $m$，每个背包有容量上限 $t_i$.&lt;/p&gt;
&lt;p&gt;物品 $i$ 能够放入背包 $j$，当且仅当 $t_j \geq w_i$。&lt;/p&gt;
&lt;p&gt;现在选出 $n$ 个物品，使它们能够通过交换顺序满足：物品的重量和价值从左到右均单调不降。且这 $n$ 个物品能分别放入不同的背包中。求 $\rm{Max_n}$.&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;$\rm{Part}$ $\rm{1}$&lt;/h3&gt;
&lt;p&gt;这 $n$ 个物品需要满足两个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;物品的重量和价值从左到右均单调不降。&lt;/li&gt;
&lt;li&gt;都能放进背包中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;容易想到将 $n$ 个物品按照重量升序排序，将 $m$ 个背包按照容量升序排序。&lt;/p&gt;
&lt;h3&gt;$\rm{Part}$ $\rm{2}$&lt;/h3&gt;
&lt;p&gt;要满足性质2，可以贪心地将 $w_n$ 与 $t_m$ 对应，$w_{n-1}$ 与 $t_{m-1}$ 对应……&lt;/p&gt;
&lt;p&gt;如果这样对应之后还是无法满足性质2，那其他的对应方法一定也无法满足。&lt;/p&gt;
&lt;h3&gt;$\rm{Part}$ $\rm{3}$&lt;/h3&gt;
&lt;p&gt;现在要满足性质1.&lt;/p&gt;
&lt;p&gt;目前物品的重量已经单调不降了，只需要让价值单调不降。由于与背包对应的策略是从后往前的，&lt;/p&gt;
&lt;p&gt;考虑对物品从后往前DP。&lt;/p&gt;
&lt;p&gt;设置状态，$\operatorname{DP}_{i}$ 表示最左端的物品价值为 $i$ 时，最多选多少个物品。&lt;/p&gt;
&lt;p&gt;假设当前我们枚举到了第 $i$ 个物品。如果要选 $i$，那么已经选好的物品只有可能在 $i$ 右侧，且右侧点的价值一定都大于等于 $v_i$.&lt;/p&gt;
&lt;p&gt;记：&lt;/p&gt;
&lt;p&gt;$$
suf=\max_{k\geq v_i}(\operatorname{DP}_{k})&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;如果我们要选 $i$，那么 $i$ 一定放入第 $m-suf$ 号背包。能放入的条件为 $w_i\leq t_{m-suf}$.&lt;/p&gt;
&lt;p&gt;（也就是说，如果要选 $i$，那么一定把 $i$ 接在右侧能选的最长的数列上面，否则对后面的答案没有贡献）&lt;/p&gt;
&lt;p&gt;求 $suf$ 的过程可以使用树状数组进行优化。（把所有 $\operatorname{DP}_{i}$ 放到树状数组中）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = n; i &gt;= 1; i--){
    int suf = query(a[i].v); //求max(dp[k]), k &gt;= a[i].v
    if(a[i].w &amp;#x3C;= t[m - suf]){ //能满足条件2
        ans = max(ans, suf + 1); //upd. ans
        add(a[i].v, suf + 1); //add(v, len);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define MAXN 100008
using namespace std;

int T, n, m, t[MAXN];
struct NODE{
    int w, v;
    friend bool operator &amp;#x3C; (const NODE &amp;#x26;A, const NODE &amp;#x26;B){
        return A.w &amp;#x3C; B.w || (A.w == B.w &amp;#x26;&amp;#x26; A.v &amp;#x3C; B.v);
    }
} a[MAXN];
int c[MAXN];
int p[MAXN], cnt = 0;
void init() {
    sort(t + 1, t + m + 1);
    
    for(int i = 1; i &amp;#x3C;= n; i++) p[i] = a[i].v;
    cnt = n;
    sort(p + 1, p + cnt + 1);
    cnt = unique(p + 1, p + cnt + 1) - p - 1;
    for(int i = 1; i &amp;#x3C;= n; i++) a[i].v = lower_bound(p + 1, p + cnt + 1, a[i].v) - p;
    
    sort(a + 1, a + n + 1);
    memset(c, 0, sizeof(c));
}
int lowbit(int x) {
    return x &amp;#x26; (-x);
}
void add(int x, int val) {
    for(int i = x; i; i -= lowbit(i)) {
        c[i] = max(c[i], val);
    }
}
int query(int v) {
    int ret = 0;
    for(int i = v; i &amp;#x3C;= n; i += lowbit(i)) {
        ret = max(ret, c[i]);
    }
    return ret;
}
int main() {
    scanf(&quot;%d&quot;, &amp;#x26;T);
    while(T--) {
        scanf(&quot;%d&quot;, &amp;#x26;n);
        for(int i = 1; i &amp;#x3C;= n; i++) {
            scanf(&quot;%d%d&quot;, &amp;#x26;a[i].w, &amp;#x26;a[i].v);
        }
        scanf(&quot;%d&quot;, &amp;#x26;m);
        for(int i = 1; i &amp;#x3C;= m; i++) {
            scanf(&quot;%d&quot;, &amp;#x26;t[i]);
        }
        init();
        int ans = 0;
        for(int i = n; i &gt;= 1; i--) {
            int suf = query(a[i].v);
            if(a[i].w &amp;#x3C;= t[m - suf]) {
                ans = max(ans, suf + 1);
                add(a[i].v, suf + 1);
            }
        }
        cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
    }
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【ZROI-19普转提-Day3-T4】DAG</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-19pzt-day3-t4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-19pzt-day3-t4</guid><description>算法题解</description><pubDate>Mon, 21 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目地址：&lt;a href=&quot;http://zhengruioi.com/problem/1007&quot;&gt;【普转提七联测 Day 3】DAG - 题目 - Zhengrui Online Judge&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给出一个无向图，请你给边定向成为一个 $\rm{DAG}$，使得最长路最短。求这个最短长度。&lt;/p&gt;
&lt;p&gt;$1\leq n\leq 8$，$1\leq m\leq 20$.&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{1}$&lt;/h3&gt;
&lt;p&gt;暴力？&lt;/p&gt;
&lt;h3&gt;$\rm{subtask}$ $\rm{2}$&lt;/h3&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;/p&gt;
&lt;p&gt;于是我们只考虑颜色之间的关系。将一个颜色看作一个点，那么无向图转化为一个由颜色作为顶点的完全图。现在给这个完全图的边定向，使其成为 $\rm{DAG}$，显然其中的最长路长度为 $n-1$.&lt;/p&gt;
&lt;p&gt;按照颜色图中，各个颜色之间的关系，在原图中对应的连边，这样每个点被经过一次且无环，最长路长度为 $n-1$.&lt;/p&gt;
&lt;p&gt;此时问题转化为：求无向图的最小染色数，也就是求无向图最少由多少个独立集组成。&lt;/p&gt;
&lt;p&gt;观察数据范围，一眼看出状压$\rm{DP}$.&lt;/p&gt;
&lt;p&gt;将每个点在集合中是存在进行二进制压缩，设置状态 $\operatorname{DP}_{i}$ 表示 $i$ 这个集合，最少由多少个独立集组成。&lt;/p&gt;
&lt;p&gt;枚举 $i$ 的所有子独立集进行转移即可。&lt;/p&gt;
&lt;p&gt;时间复杂度 $\mathcal{O}(3^n)$.&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define MAXN 20
#define MAXM 200
using namespace std;
int n, m;
int u[MAXM], v[MAXM];
bool is_ind_set[(1 &amp;#x3C;&amp;#x3C; MAXN)];
int dp[(1 &amp;#x3C;&amp;#x3C; MAXN)];
int main() {
	scanf(&quot;%d%d&quot;, &amp;#x26;n, &amp;#x26;m);
	for(int i = 1; i &amp;#x3C;= m; i++) {
		scanf(&quot;%d%d&quot;, &amp;#x26;u[i], &amp;#x26;v[i]);
	}
	for(int i = 0; i &amp;#x3C; (1 &amp;#x3C;&amp;#x3C; n); i++) {
		is_ind_set[i] = true;
		for(int j = 1; j &amp;#x3C;= m; j++) {
			if((i &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (u[j] - 1))) &amp;#x26;&amp;#x26; (i &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (v[j] - 1)))) {
				is_ind_set[i] = false;
				break;
			}
		}
	}
	memset(dp, 0x3f, sizeof(dp));
	dp[0] = 0;
	for(int i = 0; i &amp;#x3C; (1 &amp;#x3C;&amp;#x3C; n); i++) {
		for(int j = i; j; j = (j - 1) &amp;#x26; i) {
			if(is_ind_set[j]) {
				dp[i] = min(dp[i ^ j] + 1, dp[i]);
			}
		}
	}
	printf(&quot;%d\n&quot;, dp[(1 &amp;#x3C;&amp;#x3C; n) - 1] - 1);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【洛谷-P5664】Emiya家今天的饭</title><link>https://www.tonyyin0418.com/blog/oi-solution/p5664</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/p5664</guid><description>算法题解</description><pubDate>Sun, 20 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目地址：&lt;a href=&quot;https://www.luogu.com.cn/problem/P5664&quot;&gt;P5664 Emiya 家今天的饭 - 洛谷 | 计算机科学教育新生态&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;一共 $n$ 种烹饪方法和 $m$ 种主要食材，每道菜对应一种方法和一种食材。使用第 $i$ 种方法和 第 $j$ 种食材，可以做出 $a_{i, j}$ 道不同的菜。求有多少个集合满足如下条件：&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;p&gt;数据范围：$n\leq 100, m\leq 2000$.&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;p&gt;容易想到DP，不容易想到状态。&lt;/p&gt;
&lt;p&gt;期望复杂度 $n^2m$.&lt;/p&gt;
&lt;h4&gt;$\rm{Subtask}$ $\rm{1}$&lt;/h4&gt;
&lt;p&gt;对于 $32%$ 的数据，暴搜即可，与正解思路无关…&lt;/p&gt;
&lt;h4&gt;$\rm{Subtask}$ $\rm{2}$&lt;/h4&gt;
&lt;p&gt;对于 $84%$ 的数据，期望复杂度 $mn^3$ .&lt;/p&gt;
&lt;p&gt;性质 $1, 2$ 很好满足，如果没有性质 $3$ 这题随便做，所以具体分析性质 $3$.&lt;/p&gt;
&lt;p&gt;容易想到容斥。对于一个满足性质 $1, 2$ 但不满足性质 $3$ 的集合，显然有且&lt;strong&gt;仅有一种&lt;/strong&gt;主要食材超过了集合大小的一半。所以答案等于：&lt;/p&gt;
&lt;p&gt;$$
\text{满足性质 }1, 2 \text{ 的方案数量}-\text{某一列超过了集合大小一半的方案数量}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;分开解决这两个问题。&lt;/p&gt;
&lt;h6&gt;第一部分：求满足性质 $1, 2$ 的方案数量&lt;/h6&gt;
&lt;p&gt;根据乘法原理，满足性质 $1, 2$ 的方案数量为：&lt;/p&gt;
&lt;p&gt;$$
\left[\prod_{i=1}^{n}(\sum_{j = 1}^{m}a_{i, j} + 1)\right]-1
$$&lt;/p&gt;
&lt;h6&gt;第二部分：求满足性质 $1, 2$，不满足性质 $3$ 的方案数量&lt;/h6&gt;
&lt;p&gt;由于仅有一种主要食材超过集合大小的一半，考虑枚举这种主要食材。&lt;/p&gt;
&lt;p&gt;假设枚举到不合法的食材为 $t$，那么此时我们只关心：选择的方法是否对应食材 $t$.&lt;/p&gt;
&lt;p&gt;接下来设计状态。$\operatorname{DP}_{i, j, k}$ 表示对于已经选定的食材 $t$，前 $i$ 种方法中，有 $j$ 种方法与食材 $t$ 对应，有 $k$ 种方法不与食材 $t$ 对应，那么有转移方程：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{i, j, k}=\operatorname{DP}&lt;/em&gt;{i-1, j, k}+a_{i, t}\times\operatorname{DP}&lt;em&gt;{i-1, j-1, k}+(\sum&lt;/em&gt;{r=1}^{m}a_{i, r}-a_{i, t})\times \operatorname{DP}_{i-1, j, k-1}
$$&lt;/p&gt;
&lt;p&gt;这样我们可以很方便的求出不合法方案数：&lt;/p&gt;
&lt;p&gt;$$
\sum_{1\leq k&amp;#x3C;j\leq n} \operatorname{DP}_{n, j, k}
$$&lt;/p&gt;
&lt;p&gt;第一部分复杂度为 $\mathcal{O}(mn)$，第二部分 $\sum_{r=1}^{m}a_{i, r}$ 可以 $\mathcal{O}(mn)$ 预处理，DP 复杂度 $\mathcal{O}(mn^3)$.&lt;/p&gt;
&lt;h4&gt;$\rm{Subtask}$ $\rm{3}$&lt;/h4&gt;
&lt;p&gt;考虑如何优化 $\rm{Subtask}$ $\rm{2}$.&lt;/p&gt;
&lt;p&gt;上面的不合法方案数统计方法：&lt;/p&gt;
&lt;p&gt;$$
\sum_{1\leq k&amp;#x3C;j\leq n} \operatorname{DP}_{n, j, k}
$$&lt;/p&gt;
&lt;p&gt;其中 $k, j$ 满足的条件是 $k&amp;#x3C;j$，移项可以得到 $j-k&gt;0$. 所以判断一个方案是否合法，只需要判断 $j-k$ 的符号，于是将 $\rm{Subtask}$ $\rm{2}$ 状态中的 $j$ 和 $k$ 压为一维 $j-k$ 的值，复杂度为 $\mathcal{O}(n^2m)$，转移类似。&lt;/p&gt;
&lt;p&gt;于是此时状态：$\operatorname{DP}_{i, j}$ 表示对于已经选定的食材 $t$，前 $i$ 种方法中，有 $a$ 种方法与食材 $t$ 对应，有 $b$ 种方法不与食材 $t$ 对应，$j = b - a$.&lt;/p&gt;
&lt;p&gt;转移方程：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{i, j} = \operatorname{DP}&lt;/em&gt;{i-1, j} + a_{i,t}\times \operatorname{DP}&lt;em&gt;{i-1, j-1}+(\sum&lt;/em&gt;{r=1}^{m}a_{i, r}-a_{i, t})\times \operatorname{DP}_{i-1, j+1}
$$&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define int long long
#define MAXN 108
#define MAXM 2008
using namespace std;
const int mod = 998244353;
int read() {
    int ret = 0; char ch = getchar();
    while(ch &amp;#x3C; &apos;0&apos; || ch &gt; &apos;9&apos;) ch = getchar();
    while(ch &amp;#x3C;= &apos;9&apos; &amp;#x26;&amp;#x26; ch &gt;= &apos;0&apos;) ret = ret * 10 + ch - &apos;0&apos;, ch = getchar();
    return ret;
}
int n, m;
int a[MAXN][MAXM], sum[MAXN];
int dp[MAXN][2 * MAXN];
signed main() {
    n = read(); m = read();
    int ans = 1;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        for(int j = 1; j &amp;#x3C;= m; j++) {
            a[i][j] = read();
            sum[i] = (sum[i] + a[i][j]) % mod;
        }
        ans = ans * (sum[i] + 1) % mod;
    }
    for(int t = 1; t &amp;#x3C;= m; t++) {
        memset(dp, 0, sizeof(dp));
        dp[0][n] = 1;
        for(int i = 1; i &amp;#x3C;= n; i++) {
            for(int j = n - i; j &amp;#x3C;= n + i; j++) {
                dp[i][j] = (dp[i - 1][j] + a[i][t] * dp[i - 1][j - 1] % mod + (sum[i] - a[i][t]) * dp[i - 1][j + 1] % mod) % mod;
            }
        }
        for(int j = 1; j &amp;#x3C;= n; j++) {
            ans = ((ans - dp[n][n + j]) % mod + mod) % mod;
        }
    }
    cout &amp;#x3C;&amp;#x3C; ((ans - 1) % mod + mod) % mod &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>【ZROI-19普及组-Day2-T4】与非门树</title><link>https://www.tonyyin0418.com/blog/oi-solution/zroi-19pj-day2-t4</link><guid isPermaLink="true">https://www.tonyyin0418.com/blog/oi-solution/zroi-19pj-day2-t4</guid><description>算法题解</description><pubDate>Sat, 19 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个节点的有根树，每个点有点权。对于所有叶子节点，点权为 $0$ 或 $1$。非叶子节点分为两类，对于第一类，点权为其所有儿子节点的&lt;strong&gt;点权与非和&lt;/strong&gt;；对于第二类，点权恒为 $h_i\in[0, 1]$，题目给定 $h_i$.&lt;/p&gt;
&lt;p&gt;现在给叶子节点赋值。&lt;/p&gt;
&lt;p&gt;存在 $A$ 种不同的赋值方法使得：&lt;strong&gt;若将所有第二类点变为第一类点，根节点的点权不变&lt;/strong&gt;，一共存在 $B$ 种不同的赋值方法。&lt;/p&gt;
&lt;p&gt;求 $\frac{A}{B}\bmod 998244353$.&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;p&gt;~上面的部分只用于理解题意，与Solution部分基本无关。~&lt;/p&gt;
&lt;p&gt;考虑树形DP.&lt;/p&gt;
&lt;p&gt;定义 $\operatorname{cnt}[x]$ 表示与 $x$ 直接相连的叶子节点个数。&lt;/p&gt;
&lt;p&gt;定义 $x$ 的理论值为：如果所有的与非门都能正常工作，$x$ 的点权。&lt;/p&gt;
&lt;p&gt;定义 $x$ 的实际值为：在实际情况下， $x$ 的点权。&lt;/p&gt;
&lt;p&gt;设置状态 $\operatorname{DP}_{x,0/1,0/1}$ 表示 $x$ 的点权，理论值为 $0/1$，实际值为 $0/1$ 时的赋值方法种类数量。&lt;/p&gt;
&lt;h3&gt;$\rm{Part}$ $\rm{1}$&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;如果 $x$ 不是一个坏了的与非门&lt;/strong&gt;，那么考虑此时四种状态如何从 $x$ 的儿子转移上来。&lt;/p&gt;
&lt;h5&gt;$\operatorname{DP}_{x, 0, 0}$ 的转移&lt;/h5&gt;
&lt;p&gt;对于 $\operatorname{DP}_{x, 0, 0}$，理论值和实际值都为 $0$，所以对于所有 $x$ 的儿子，理论值和实际值必须都为 $1$，即：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{x, 0, 0} = \prod&lt;/em&gt;{\text{i是x的儿子}}\operatorname{DP}_{i, 1, 1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h5&gt;$\operatorname{DP}_{x, 0, 1}$ 的转移&lt;/h5&gt;
&lt;p&gt;对于 $\operatorname{DP}_{x, 0, 1}$，理论值为 $0$，实际值为 $1$，说明 $x$ 的所有儿子，必须满足理论值为 $1$，且至少一个实际值不为 $1$&lt;/p&gt;
&lt;p&gt;容易想到容斥原理，易得 $\operatorname{DP}_{x, 0, 1}$ 等于：所有儿子的理论值都为 $1$，实际值随意的情况数，减去所有儿子理论值都为 $1$ ，实际值都为 $1$ 的情况数。&lt;/p&gt;
&lt;p&gt;又因为所有儿子理论值都为 $1$ ，实际值都为 $1$ 的情况数等于 $\operatorname{DP}_{x, 0, 0}$，所以有转移方程：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{x, 0, 1}=\prod&lt;/em&gt;{\text{i是x的儿子}}(\operatorname{DP}&lt;em&gt;{i, 1, 0} +\operatorname{DP}&lt;/em&gt;{i, 1, 1} )-\operatorname{DP}_{x, 0, 0}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h5&gt;$\operatorname{DP}_{x, 1, 0}$ 的转移&lt;/h5&gt;
&lt;p&gt;与 $\operatorname{DP}_{x, 0, 1}$ 类似，可以得到转移方程：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{x, 1, 0}=\prod&lt;/em&gt;{\text{i是x的儿子}}(\operatorname{DP}&lt;em&gt;{i, 0, 1} +\operatorname{DP}&lt;/em&gt;{i, 1, 1} )-\operatorname{DP}_{x, 0, 0}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h5&gt;$\operatorname{DP}_{x, 1, 1}$ 的转移&lt;/h5&gt;
&lt;p&gt;由于 $\operatorname{DP}&lt;em&gt;{x, 0, 0}$，$\operatorname{DP}&lt;/em&gt;{x, 0, 1}$，$\operatorname{DP}&lt;em&gt;{x, 1, 0}$，$\operatorname{DP}&lt;/em&gt;{x, 1, 1}$ 四种情况为所有的状态，所以只需要用所有的状态减去前面三个状态，就可以得到 $\operatorname{DP}_{x, 1, 1}$ 了，即：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{DP}&lt;em&gt;{x, 1, 1}=2^{\operatorname{cnt}[x]}\cdot \prod&lt;/em&gt;{\text{i是x的儿子}}(\operatorname{DP}&lt;em&gt;{i, 0, 0}+\operatorname{DP}&lt;/em&gt;{i, 0, 1}+\operatorname{DP}&lt;em&gt;{i, 1, 0}+\operatorname{DP}&lt;/em&gt;{i, 1, 1})-(\operatorname{DP}&lt;em&gt;{x, 0, 0}+\operatorname{DP}&lt;/em&gt;{x, 0, 1}+\operatorname{DP}_{x, 1, 0})&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;至此，我们已经知道了：若 $x$ 不是一个坏了的与非门，如何从 $x$ 的儿子转移到 $x$ 上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void dfs(int x) {
    dp[x][0][0] = dp[x][0][1] = dp[x][1][0] = 1;
    dp[x][1][1] = power(2, cnt[x]);
    for(int i = head[x]; i; i = edge[i].nxt) {
        int ne = edge[i].to;
        dfs(ne);
        dp[x][0][0] *= dp[ne][1][1];
        dp[x][0][1] *= dp[ne][1][0] + dp[ne][1][1];
        dp[x][1][0] *= dp[ne][0][1] + dp[ne][1][1];
        dp[x][1][1] *= dp[ne][0][0] + dp[ne][0][1] + dp[ne][1][0] + dp[ne][1][1];
    }
    dp[x][0][1] -= dp[x][0][0];
    dp[x][1][0] -= dp[x][0][0];
    dp[x][1][1] -= dp[x][0][0] + dp[x][0][1] + dp[x][1][0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;$\rm{Part}$ $\rm{2}$&lt;/h3&gt;
&lt;p&gt;下面处理特殊情况：$x$ 是坏了的与非门。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果 $x$ 只能输出 $0$，不能输出 $1$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为 $\rm{Part}$ $\rm{1}$ 中的实际值，是在默认 $x$ 不是坏了的与非门的前提下考虑的。如果 $x$ 是坏了的与非门，那么 $\operatorname{DP}_{x}$ 就只与 $x$ 儿子的理论值相关了。&lt;/p&gt;
&lt;p&gt;所以 $\rm{Part}$ $\rm{1}$ 中的 $\operatorname{DP}&lt;em&gt;{x, 1, 1}$ 应该归到 $\operatorname{DP}&lt;/em&gt;{x, 1, 0}$ 中，$\operatorname{DP}&lt;em&gt;{x, 0, 1}$ 应该归到 $\operatorname{DP}&lt;/em&gt;{x, 0, 0}$ 中，即：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if(t[now].sta == 0) {
    dp[now][0][0] += dp[now][0][1];
    dp[now][1][0] += dp[now][1][1];
    dp[now][0][1] = dp[now][1][1] = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果 $x$ 只能输出 $1$，不能输出 $0$.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同理，将 $\rm{Part}$ $\rm{1}$ 中的 $\operatorname{DP}&lt;em&gt;{x, 0, 0}$ 应该归到 $\operatorname{DP}&lt;/em&gt;{x, 0, 1}$ 中，$\operatorname{DP}&lt;em&gt;{x, 1, 0}$ 应该归到 $\operatorname{DP}&lt;/em&gt;{x, 1, 1}$ 中.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if(t[now].sta == 1) {
    dp[now][0][1] += dp[now][0][0];
    dp[now][1][1] += dp[now][1][0];
    dp[now][0][0] = dp[now][1][0] = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;代码没开long long，没取模（因为加上取模之后代码宽度直接爆炸）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define MAXN 200008
using namespace std;
int n, root;
struct Point{int fa, cnt_son, sta;} t[MAXN];
struct Edge{int to, nxt;} edge[2 * MAXN];
int head[MAXN], cnt = 0;
int deg[MAXN], res[MAXN];
void add(int x, int y) {
    edge[++cnt].to = y;
    edge[cnt].nxt = head[x];
    head[x] = cnt;
    deg[x]++;
}
int dp[MAXN][2][2];
int power(int x, int k) {
    int ret = 1;
    while(k) {
        if(k &amp;#x26; 1) ret = ret * x;
        k &gt;&gt;= 1;
        x = x * x;
    }
    return ret;
}
int inv(int x, int p) {
    return power(x, p - 2) % p;
}
void dfs(int now) {
    res[now] = t[now].cnt_son - deg[now];
    dp[now][0][0] = dp[now][0][1] = dp[now][1][0] = 1;
    dp[now][1][1] = power(2, res[now]);
    for(int i = head[now]; i; i = edge[i].nxt) {
        int ne = edge[i].to;
        dfs(ne);
        res[now] += res[ne];
        dp[now][0][0] *= dp[ne][1][1];
        dp[now][0][1] *= dp[ne][1][0] + dp[ne][1][1];
        dp[now][1][0] *= dp[ne][0][1] + dp[ne][1][1];
        dp[now][1][1] *= dp[ne][0][0] + dp[ne][0][1] + dp[ne][1][0] + dp[ne][1][1];
    }
    dp[now][0][1] -= dp[now][0][0];
    dp[now][1][0] -= dp[now][0][0];
    dp[now][1][1] -= dp[now][0][0] + dp[now][0][1] + dp[now][1][0];
    if(t[now].sta == 0) {
        dp[now][0][0] += dp[now][0][1];
        dp[now][1][0] += dp[now][1][1];
        dp[now][0][1] = dp[now][1][1] = 0;
    }
    if(t[now].sta == 1) {
        dp[now][0][1] += dp[now][0][0];
        dp[now][1][1] += dp[now][1][0];
        dp[now][0][0] = dp[now][1][0] = 0;
    }
}
int main() {
    cin &gt;&gt; n;
    for(int i = 1; i &amp;#x3C;= n; i++) {
        cin &gt;&gt; t[i].fa &gt;&gt; t[i].cnt_son &gt;&gt; t[i].sta;
        if(t[i].fa == 0) {
            root = i;
            continue;
        }
        add(t[i].fa, i);
    }
    dfs(root);
    int ans = (dp[root][0][0] + dp[root][1][1]) * inv(power(2, res[root]), 998244353);
    cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;</content:encoded></item></channel></rss>