<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>wegret&apos;s blog</title><description>刀剑阵易过，情人关难闯。</description><link>https://fuwari.vercel.app/</link><language>zh_CN</language><item><title>word2vec原理以及C代码详解</title><link>https://fuwari.vercel.app/posts/nlp/word2vec/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/nlp/word2vec/</guid><pubDate>Tue, 03 Feb 2026 15:48:07 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面是我的word2vec的算法理解到原始代码分析。叠甲：都是我的个人理解，可能有不对的地方。
做这个是因为我的毕设是word2vec的CUDA加速，所以要先读懂原始的C代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;word2vec是什么？&lt;/p&gt;
&lt;p&gt;我的理解就是，不同的单词有语义，含义不同。语义用一个一定维度的向量来表示。&lt;/p&gt;
&lt;p&gt;而word2vec算法就是通过海量的语料，算出每个单词的语义。&lt;/p&gt;
&lt;h2&gt;word2vec原理&lt;/h2&gt;
&lt;h3&gt;整体思路&lt;/h3&gt;
&lt;p&gt;从头开始，我们最初的目标，肯定是预测语义——输入一个单词（单词的ID编号），然后输出这个语义向量。&lt;/p&gt;
&lt;p&gt;但是有个问题来了，没有人知道这个语义长什么样。&lt;/p&gt;
&lt;p&gt;没有一个数据集能直接指出：&lt;code&gt;苹果&lt;/code&gt;的语义向量应该是 &lt;code&gt;[0.3, -0.7, 0.2, ...]&lt;/code&gt;。这个标注根本不存在。&lt;/p&gt;
&lt;p&gt;那怎么办呢？&lt;/p&gt;
&lt;p&gt;语言学有一个经典假说，叫&lt;strong&gt;分布式假说&lt;/strong&gt;（Distributional Hypothesis）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一个词的含义，由它经常出现在什么样的上下文中决定。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如你不认识&quot;榴莲&quot;这个词，但你反复看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;我喜欢吃榴莲&quot;
&quot;榴莲很好吃&quot;
&quot;买了一个榴莲&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自然会推断：榴莲是一种食物。你是通过&lt;strong&gt;它周围的词&lt;/strong&gt;来理解它的意思的。&lt;/p&gt;
&lt;p&gt;所以，word2vec的思路是，既然语义是没法直接去拟合的，那就通过训练，去让模型尝试在训练之中学会这个语义。&lt;/p&gt;
&lt;p&gt;举个例子，假设学生在学英语的时候，老师要尝试测试学生是否真的理解了单词的意思，可以用两种方式考学生：&lt;/p&gt;
&lt;p&gt;方法一：完形填空。老师可以遮住句子中间一个词，给你看周围的词，让你猜中间是什么。&lt;/p&gt;
&lt;p&gt;比如，输入&lt;code&gt;今天&lt;/code&gt;,&lt;code&gt;天气&lt;/code&gt;,&lt;code&gt;___&lt;/code&gt;,&lt;code&gt;不错&lt;/code&gt;。你就能猜到是&lt;code&gt;真&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果猜到了，你可能就已经学会了这个语义。&lt;/p&gt;
&lt;p&gt;方法二：联想。给你一个中间的词，让你猜这个词的两边，通常会出现什么词。（我的理解是，类似于给你一个词，让你去解释。）&lt;/p&gt;
&lt;p&gt;这里就是word2vec的两种训练方法。方法一就是CBOW，方法二就是skip-gram。&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;模型第一步，就是输入一个词ID（或上下文的词ID），然后去一个词表（目前的词义表）里面去查这个向量。然后用这个向量，去跑模型，算最后的预测的词ID，或者预测的上下文。&lt;/p&gt;
&lt;p&gt;这就是整个算法的流程。&lt;/p&gt;
&lt;h3&gt;模型设计&lt;/h3&gt;
&lt;p&gt;大致知道整个训练过程后，下面是比较关键的问题——这个模型（输入上下文词ID，输出预测词ID）到底怎么设计，怎么训练的呢？&lt;/p&gt;
&lt;p&gt;word2vec的模型结构非常简单，它不是深度神经网络，核心就是一个点积+sigmoid。&lt;/p&gt;
&lt;p&gt;首先，我们回到数学上来，两个向量像不像，怎么判断呢？&lt;/p&gt;
&lt;p&gt;——点积。
$$
Score = v \cdot u = \sum v_i \times u_i
$$
如果两个词是类似的，方向一致，语义相似。点积结果就是一个很大的正数。如果方向相反，就是很大的负数。如果垂直（没啥关系），就是0。&lt;/p&gt;
&lt;h4&gt;CBOW&lt;/h4&gt;
&lt;h5&gt;相关性&lt;/h5&gt;
&lt;p&gt;前面说过了，CBOW类似完形填空。&lt;/p&gt;
&lt;p&gt;假设我们要预测&lt;code&gt;今天 天气 [真] 不错&lt;/code&gt;的&lt;code&gt;真&lt;/code&gt;字。&lt;/p&gt;
&lt;p&gt;step 1：输入层（input）&lt;/p&gt;
&lt;p&gt;CBOW是不在乎语序的，直接把我们要预测的那个词的附近几个词全部找出来，根据ID找到词向量（假设我们的模型目前把这个词预测成 $V_{word}$ 了），然后直接相加（也可以求均值），变成上下文向量
$$
V_{Context} = V_{今天} + V_{天气} + V_{不错}
$$
这个向量就代表上下文。&lt;/p&gt;
&lt;p&gt;step 2：预测层。也就是做点积。&lt;/p&gt;
&lt;p&gt;前面的上下文向量 $V_{context}$ 可以视为这个预测模型的输入，我们现在要做什么呢？既然我们这个模型要预测最相关的词，也就是点积最大的，假设语料库里总共有 $N$ 个词，每个词目前的预测词义是 $V_i$ （是一个向量），那么我们去算出所有的
$$
o_i=(V_{context}, V_i)
$$
然后找到最大的 $o_j=\max_{i\in [1,N]} o_i$，也就是说，所有词中，$V_j$ 是目前和这个上下文最相关的词了（因为点积最大）！这就是我们预测的结果！&lt;/p&gt;
&lt;p&gt;好，现在来检查答案，如果 $V_j$ 就是结果的话，那我们就猜对了。如果 $V_j$ 不是结果，我们就猜错了。&lt;/p&gt;
&lt;p&gt;那现在问题来了，在训练时更新权重的时候，我们要知道，正确的输出是什么对吧。那正确的输出是什么呢？&lt;/p&gt;
&lt;p&gt;因为我们知道，这个点积越大越好，没有上限的，我们总不能把正确的输出设置成无穷吧？所以这个点积要过一个非线性修正器，也就是激活函数，把无穷的上限映射成1，把负无穷这个下限映射成-1或者0。&lt;/p&gt;
&lt;p&gt;这样我们就可以给出正确的输出了——在我们那个的 $V_{answer}$ 下，点积再过一个激活函数得到的正确结果，应该是1；如果不是 $V_{answer}$，那就是 0。&lt;/p&gt;
&lt;h5&gt;激活函数&lt;/h5&gt;
&lt;p&gt;然后来考虑这个激活函数。&lt;/p&gt;
&lt;p&gt;按照我在看这个算法的时候的考虑，我们这里要加激活函数的原因很简单，把无穷和负无穷映射到0和1，所以我们只需要一个Sigmoid激活函数
$$
P = \frac{1}{1 + e^{-o_i}}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过，另一种理论上的做法是 softmax激活函数。
$$
P = \frac{e^{o_i}}{\sum e^{o_k}}
$$
这种要把所有的概率和搞成1。&lt;/p&gt;
&lt;p&gt;这样的好处是很直观啊，直接反向传播也很自然。&lt;/p&gt;
&lt;p&gt;但是从头开始，我的思路是这样的，每个词去算相似度（不是二分类），互相不需要归一化，因为在预测的时候，直接取max就行了。然后如果训练，就针对正确的那个，结果设置成过完激活函数的1。其他的设置成0。&lt;/p&gt;
&lt;p&gt;所以我们只需要一个映射到有界的sigmoid函数就行了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;工程优化问题：负采样&lt;/h5&gt;
&lt;p&gt;到这里，还有什么问题吗？&lt;/p&gt;
&lt;p&gt;然后重新来回顾训练，CBOW中，我们的训练数据，是一堆上下文作为输入，正确的中心词作为输出。&lt;/p&gt;
&lt;p&gt;我们上下文向量 $V_{context}$ 输入进去，先去更新正确的中心词（假设是 $V_i$ ）这条路径 （也就是路径 $i$ ），我们毫无疑问，要求 $sigmoid(V_{context}\cdot V_i)=1$，根据我们的要求与实际的输出，算loss，然后在这条路径进行反向传播。&lt;/p&gt;
&lt;p&gt;其他路径呢？其他路径，我们一定是希望 $sigmoid(V_{context}\cdot V_i)\neq 1$，反正不能是最大的，一般是 $0$ ，因为不是我们期望的最大输出。同理我们这样更新loss，反向传播。&lt;/p&gt;
&lt;p&gt;这对吗？这显然不太对，有一个重要问题：&lt;/p&gt;
&lt;p&gt;假设 $N$ 个单词，那我不是每次训练，要更新 $1$ 次正确向量，$N-1$ 次不正确向量？（而实际上 $N$ 是比较大的，无论是从样本均匀性还是计算量来说，都不太合适。）&lt;/p&gt;
&lt;p&gt;这肯定不合适，所以在工程优化上，word2vec不是这样做的，而是采用了&lt;strong&gt;负采样（Negative Sampling）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;正样本，就是中心词，我们依然要求 sigmoid的结果是 1。&lt;/li&gt;
&lt;li&gt;负样本，我们是不会全部更新的，我们随机抽取 $k$ 个不相关的词，要求它的结果是 0。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于剩下的 $N-1-k$ 的样本呢？我们单次迭代，就不管了，因为语料库很大啊，迭代次数足够多，每个词迟早会被抽中的，只要训练够久，都会被推到正确的位置。&lt;/p&gt;
&lt;h5&gt;两套矩阵&lt;/h5&gt;
&lt;p&gt;前面我推导的时候，上下文向量 $V_{context}$ 是由周围的词的向量加和得到的，然后预测的时候，拿 $V_{context}$ 和中心词的向量 $V_{center}$ 做点积。这里我们前面暂时默认了，一个单词，作为上下文的时候，和作为中心词的时候，两个向量都是从同一个词表中取出来的。&lt;/p&gt;
&lt;p&gt;现在还剩最后一个问题：这样可以吗？&lt;/p&gt;
&lt;p&gt;假设只有一套矩阵 $W$，一个词会在两种场景下被更新：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它作为别的词的上下文的时候。&lt;/li&gt;
&lt;li&gt;它作为被预测的目标的时候。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两个可能是矛盾的，会干扰训练，总之，我们分出两个矩阵，一个&lt;code&gt;syn0&lt;/code&gt;只管上下文中的语义，&lt;code&gt;syn1&lt;/code&gt;只管目标词的语义。互不干扰。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个似乎是工程上的优化。理论上一套矩阵应该也是能跑的。但是我们在实现的时候，为了更快，一般不这么做。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到这里为止，可以直接跳转到后面的代码实现。&lt;/p&gt;
&lt;p&gt;[]: #CBOW+负采样	&quot;(CBOW+负采样，代码实现)&quot;&lt;/p&gt;
&lt;h4&gt;skip-gram&lt;/h4&gt;
&lt;p&gt;CBOW很不错，但是有什么不足吗？&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;code&gt;今天 天气 [真] 不错&lt;/code&gt;，我们最后训练出来的，&lt;code&gt;天气&lt;/code&gt;不会一定对应&lt;code&gt;真&lt;/code&gt;，也不会一定对应好，但是海量训练后，会趋向周围词的平均（或者是某种意义的平衡点）。&lt;/p&gt;
&lt;p&gt;不过，在实际实现上，我们选择的是输入中心词、输出上下文词。（实际上是和前面是一样的，毕竟&lt;code&gt;真&lt;/code&gt;和&lt;code&gt;天气&lt;/code&gt;两个词，互相为上下文，也互相为中心词。）&lt;/p&gt;
&lt;h2&gt;word2vec的代码详解&lt;/h2&gt;
&lt;h3&gt;CBOW+负采样&lt;/h3&gt;
&lt;p&gt;这里先把word2vec的原始的C代码中，用CBOW+负采样的代码提取出来。&lt;/p&gt;
&lt;p&gt;但是除了简单的实现，还涉及到一些工程上的优化问题。&lt;/p&gt;
&lt;h4&gt;随机采样&lt;/h4&gt;
&lt;p&gt;前面说过了，我们的负采样，是抽一个正样本的同时，随机抽 $k$ 个负样本（比如5个）。&lt;/p&gt;
&lt;p&gt;我们怎么随机呢？&lt;/p&gt;
&lt;p&gt;假设我们的语料长度为 $M$，不同的词有 $N$ 个。简单的方案有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按照不同的词来随机，每个词抽到的概率是 $\frac 1 N$&lt;/li&gt;
&lt;li&gt;按照语料来随机，或者说，每个词 $w$ 被抽到的概率就是出现的频率 $P(w)$ 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有什么问题呢？前者可能会导致，高频词和低频词抽到的概率一样，比如 &lt;code&gt;the&lt;/code&gt; , &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;Periphaneta&lt;/code&gt; 抽到的概率一样，那行吗？肯定不行吧，这样高频词可能无法很好地训练。我们希望最后算出来的词义，能区分 &lt;code&gt;apple&lt;/code&gt; 和 &lt;code&gt;banana&lt;/code&gt;，而不是能区分 &lt;code&gt;apple&lt;/code&gt; 和 &lt;code&gt;Periphaneta&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;后者呢？也不太好，因为语料库中，&lt;code&gt;the&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; 这种非常高频的词，出现的概率是很大很大的，他们会被不断训练，没什么必要。&lt;/p&gt;
&lt;p&gt;所以 word2vec 折中了一下，用这个公式
$$
P(w) = \frac{\text{count}(w)^{0.75}}{\sum \text{count}(u)^{0.75}}
$$
一个词抽到的概率，是它的词频的0.75次方，在所有词词频的0.75次方之和中的占比，适度打压极高频词，适度提升低频词的中签率。&lt;/p&gt;
&lt;h4&gt;多线程&lt;/h4&gt;
&lt;p&gt;在原始的代码中，是用CPU来做多线程的。不然训练的也太慢了。&lt;/p&gt;
&lt;p&gt;假设 $T$ 个线程，它把文件切分成 $T$ 份，每个线程领取自己的一份。&lt;/p&gt;
&lt;p&gt;同时，多线程可能会有一个问题：难道所有的线程一起反向传播，不会冲突吗？&lt;/p&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;但是word2vec最初的实现用了更巧妙的方式。它用了一个随机窗口缩放。&lt;/p&gt;
</content:encoded></item><item><title>CUDA C编程入门笔记（1）</title><link>https://fuwari.vercel.app/posts/course/cuda-c/cuda-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/cuda-c/cuda-1/</guid><pubDate>Sat, 03 Jan 2026 09:13:07 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;异构计算 = CPU（控制者）+ GPU（加速者）协同工作&lt;/p&gt;
&lt;p&gt;“&lt;strong&gt;为任务选择适合它的架构&lt;/strong&gt;”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;费脑子的事（逻辑、分支预测、IO读取）给 CPU。&lt;/li&gt;
&lt;li&gt;费体力的事（大规模矩阵运算、像素处理）给 GPU。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251219032707822.png&quot; alt=&quot;image-20251219032707822&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;用GPU输出：&lt;code&gt;Hello world!&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;p&gt;检查CUDA编译器是否正确安装，以及路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which nvcc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常结果是&lt;code&gt;/usr/local/cuda/bin/nvcc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;检查GPU：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l /dev/nv*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这里的输出结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;crw-rw-rw- 1 root root     195,   0 11月 24 15:55 /dev/nvidia0
crw-rw-rw- 1 root root     195,   1 11月 24 15:55 /dev/nvidia1
crw-rw-rw- 1 root root     195,   2 11月 24 15:55 /dev/nvidia2
crw-rw-rw- 1 root root     195,   3 11月 24 15:55 /dev/nvidia3
crw-rw-rw- 1 root root     195, 255 11月 24 15:55 /dev/nvidiactl
crw-rw-rw- 1 root root     235,   0 11月 24 16:10 /dev/nvidia-uvm
crw-rw-rw- 1 root root     235,   1 11月 24 16:10 /dev/nvidia-uvm-tools

/dev/nvidia-caps:
total 0
cr-------- 1 root root 238, 1 11月 24 15:55 nvidia-cap1
cr--r--r-- 1 root root 238, 2 11月 24 15:55 nvidia-cap2

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/dev/nvidia0&lt;/code&gt; 到 &lt;code&gt;/dev/nvidia3&lt;/code&gt;表示有4张独立显卡。&lt;/p&gt;
&lt;h3&gt;第一个代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void hello(void){
    printf(&quot;Hello world from GPU !\n&quot;);
}

int main(){
    printf(&quot;Hello world from CPU !\n&quot;);
    hello&amp;lt;&amp;lt;&amp;lt;1,10&amp;gt;&amp;gt;&amp;gt; ();
    cudaDeviceReset();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我保存到&lt;code&gt;test.cu&lt;/code&gt;中，然后运行&lt;code&gt;nvcc test.cu -o test&lt;/code&gt;编译，&lt;code&gt;./test&lt;/code&gt;来运行。运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./test
Hello world from CPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
Hello world from GPU !
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，输出了一次CPU，十次GPU。&lt;/p&gt;
&lt;p&gt;我的理解是，&lt;code&gt;.cu&lt;/code&gt;文件，实际上是一个C程序，只是通过CUDA调用了GPU。而&lt;code&gt;nvcc&lt;/code&gt;是一个编译器驱动，调用&lt;code&gt;gcc&lt;/code&gt;编译CPU代码、以及NVDIA的自己的编译器来翻译GPU语言。&lt;/p&gt;
&lt;p&gt;在CUDA编程里面，没有 &lt;code&gt;__global__&lt;/code&gt; 的函数，是普通C函数，比如&lt;code&gt;main&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;加上 &lt;code&gt;__global__&lt;/code&gt; 的函数（也叫 Kernel 核函数），由CPU来调用，通过GPU来执行。&lt;/p&gt;
&lt;p&gt;因此前面CPU只输出一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    printf(&quot;Hello world from CPU !\n&quot;);	// CPU运行，只有一个主线程，所以只打印一次
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(){
    printf(&quot;Hello world from CPU !\n&quot;); 
    hello&amp;lt;&amp;lt;&amp;lt;1,10&amp;gt;&amp;gt;&amp;gt; ();	// 启动1个线程块，每个块10个线程同时跑，所以打印10次
    // ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三重尖括号意味着从主线程到设备端代码的调用，格式是&lt;code&gt;函数名&amp;lt;&amp;lt;&amp;lt; 组的数量, 每组的人数 &amp;gt;&amp;gt;&amp;gt;(参数);&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;块和线程&lt;/h3&gt;
&lt;p&gt;前面这段代码，如果运行&lt;code&gt;hello&amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;和&lt;code&gt;hello&amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;，效果应该是一样的。&lt;/p&gt;
&lt;p&gt;但是其实应该是不一样的。&lt;/p&gt;
&lt;p&gt;用下面这段修改后的代码，打印出块号&lt;code&gt;blockIdx.x&lt;/code&gt;和线程号&lt;code&gt;threadIdx.x&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void hello(void){
    printf(&quot;Block %d,\t Thread %d\n&quot;, blockIdx.x, threadIdx.x);
}

int main(){
    printf(&quot;Case : &amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;\n&quot;);
    hello&amp;lt;&amp;lt;&amp;lt;1,10&amp;gt;&amp;gt;&amp;gt; ();
    cudaDeviceSynchronize();

    printf(&quot;Case : &amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;\n&quot;);
    hello&amp;lt;&amp;lt;&amp;lt;2, 5&amp;gt;&amp;gt;&amp;gt; ();
    cudaDeviceSynchronize();
    return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./test
Case : &amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;
Block 0,         Thread 0
Block 0,         Thread 1
Block 0,         Thread 2
Block 0,         Thread 3
Block 0,         Thread 4
Block 0,         Thread 5
Block 0,         Thread 6
Block 0,         Thread 7
Block 0,         Thread 8
Block 0,         Thread 9
Case : &amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;
Block 0,         Thread 0
Block 0,         Thread 1
Block 0,         Thread 2
Block 0,         Thread 3
Block 0,         Thread 4
Block 1,         Thread 0
Block 1,         Thread 1
Block 1,         Thread 2
Block 1,         Thread 3
Block 1,         Thread 4

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要分块？这看上去运行&lt;code&gt;hello&amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;和&lt;code&gt;hello&amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;结果并没有差很多，都是多个同时运行。&lt;/p&gt;
&lt;p&gt;应该是应该硬件限制，每个块的线程有上限。&lt;/p&gt;
&lt;p&gt;而且同一个block内部，有共享内存。&lt;/p&gt;
&lt;p&gt;可以提升可扩展性。让程序可以在不同的CPU上都能跑。&lt;/p&gt;
&lt;h3&gt;同步与重置&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cudaDeviceReset()&lt;/code&gt;、&lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt;的问题。&lt;/p&gt;
&lt;p&gt;理论上，&lt;code&gt;cudaDeviceReset&lt;/code&gt;是全部清空上下文，重新初始化整个环境。&lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt;是等前面的GPU干完了，不会清空上下文。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt;意思是，等前面调用的GPU代码都干完了。如果没有调用这个，前面没有干完，CPU也会继续往下。因为CPU调用核函数是非阻塞的。&lt;/p&gt;
&lt;p&gt;在前面运行这段代码的时候：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void hello(void){
    printf(&quot;Block %d,\t Thread %d\n&quot;, blockIdx.x, threadIdx.x);
}

int main(){
    printf(&quot;Case : &amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;\n&quot;);
    hello&amp;lt;&amp;lt;&amp;lt;1,10&amp;gt;&amp;gt;&amp;gt; ();
    cudaDeviceSynchronize();

    printf(&quot;Case : &amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;\n&quot;);
    hello&amp;lt;&amp;lt;&amp;lt;2, 5&amp;gt;&amp;gt;&amp;gt; ();
    cudaDeviceSynchronize();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然可以注意到，第一次打印出来&lt;code&gt;Case : &amp;lt;&amp;lt;&amp;lt; 1, 10 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;后，要等一会儿（很明显），才能看见后续的一堆东西打印出来，而且是立即全部打印出来，第二次的&lt;code&gt;Case : &amp;lt;&amp;lt;&amp;lt; 2, 5 &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;不需要等待。&lt;/p&gt;
&lt;p&gt;第一次等待，应该是因为有一个冷启动。&lt;/p&gt;
&lt;p&gt;如果把&lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt;全部换成&lt;code&gt;cudaDeviceReset()&lt;/code&gt;，理论上应该是有区别的！毕竟说过了后者会清空上下文，但是前面这段运行起来，体感上没有区别，但是我感觉应该是有区别的。&lt;/p&gt;
&lt;p&gt;用下面的代码测试一下时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;time.h&amp;gt;

__global__ void hello(void){
    // printf(&quot;Block %d,\t Thread %d\n&quot;, blockIdx.x, threadIdx.x);
}

int main(){
    int N = 100;

    clock_t start, end;
    double cost_time;

    hello&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;();
    cudaDeviceSynchronize();

    printf(&quot;Test Synchronize: \n&quot;);

    start=clock();
    for (int i=0;i&amp;lt;N;i++){
        hello&amp;lt;&amp;lt;&amp;lt;1,1&amp;gt;&amp;gt;&amp;gt;();
        cudaDeviceSynchronize();
    }
    end=clock();
    cost_time=((double)(end-start))/CLOCKS_PER_SEC;
    printf(&quot;Synchronize cost %.4f seconds\n&quot;, cost_time);

    printf(&quot;Test Reset: \n&quot;);

    start=clock();
    for (int i=0;i&amp;lt;N;i++){
        hello&amp;lt;&amp;lt;&amp;lt;1,1&amp;gt;&amp;gt;&amp;gt;();
        cudaDeviceReset();
    }
    end=clock();
    cost_time=((double)(end-start))/CLOCKS_PER_SEC;
    printf(&quot;Reset cost %.4f seconds\n&quot;, cost_time);

    return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~$ ./test
Test Synchronize: 
Synchronize cost 0.0006 seconds
Test Reset: 
Reset cost 11.5860 seconds

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感觉还是很明显的。Reset慢很多。&lt;/p&gt;
&lt;h2&gt;CUDA编程结构&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Host (主机)&lt;/strong&gt; = &lt;strong&gt;CPU&lt;/strong&gt; + 内存。负责逻辑控制、读取文件、指挥 GPU 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Device (设备)&lt;/strong&gt; = &lt;strong&gt;GPU&lt;/strong&gt; + 显存。专门负责并行的繁重计算。&lt;/p&gt;
&lt;p&gt;这两者通过 PCI-Express 总线连接，但它们的&lt;strong&gt;内存是物理隔离的&lt;/strong&gt;。这意味着，CPU 里的变量，GPU 没法直接读；GPU 算好的结果，CPU 也没法直接拿。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;可以把 CPU 上的变量名加前缀 &lt;code&gt;h_&lt;/code&gt; (host)，GPU 上的变量名加前缀 &lt;code&gt;d_&lt;/code&gt; (device)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为内存不互通，所以我们往往需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;H-&amp;gt;D：把数据从 CPU 内存（Host）拷贝到 GPU 显存（Device）。&lt;/li&gt;
&lt;li&gt;核函数执行：CPU 发令，GPU 开始并行计算。（这是异步的，CPU 发完令可以去干别的，也可以等 GPU 算完。）&lt;/li&gt;
&lt;li&gt;D-&amp;gt;H：把算好的结果从 GPU 显存拷贝回 CPU 内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在GPU分配空间，用的是&lt;code&gt;cudaMalloc&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cudaError_t cudaMalloc (void** devPtr, size_t size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个是在GPU显存上，申请一个&lt;code&gt;size&lt;/code&gt;字节的连续内存，返回的是是否分配成功，然后分配的GPU显存的指针，会被存在&lt;code&gt;devPtr&lt;/code&gt;指向的地址中。&lt;/p&gt;
&lt;p&gt;主机和设备之间的数据传输，用的是&lt;code&gt;cudaMemcpy&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cudaError_t cudaMemcpy (void* dst, const void* src, size_t count, cudaMemcpyKind kind)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从&lt;code&gt;src&lt;/code&gt;这个地址开始，复制&lt;code&gt;count&lt;/code&gt;字节到&lt;code&gt;dst&lt;/code&gt;指向的地址。&lt;/p&gt;
&lt;p&gt;最后的&lt;code&gt;kind&lt;/code&gt;，用来表示复制方向，有四种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;cudaMemcpyHostToHost&lt;/code&gt;（疑似是为了对称的，主机搬到主机，和&lt;code&gt;memcpy&lt;/code&gt;没啥区别）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cudaMemcpyHostToDevice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cudaMemcpyDeviceToHost&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cudaMemcpyDeviceToDevice&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后&lt;code&gt;cudaError_t&lt;/code&gt;是一个枚举类型，成功就是&lt;code&gt;cudaSuccess&lt;/code&gt;，失败就是&lt;code&gt;cudaErrorMemoryAllocation&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以用这个函数，把错误代码转成可以读的错误消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* cudaGetErrorString (cudaError_t error)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;向量相加&lt;/h3&gt;
&lt;p&gt;下面是一个简单的向量相加代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void kernel_add(int *d_a, int *d_b, int *d_c, int n)
{
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx &amp;lt; n)
        d_c[idx] = d_a[idx] + d_b[idx];
}

int main()
{
    int n = 1000;

    int h_a[n + 5], h_b[n + 5], h_c[n + 5];
    for (int i = 0; i &amp;lt; n; i++)
        h_a[i] = i, h_b[i] = i;

    size_t sz = n * (sizeof(int));

    int *d_a, *d_b, *d_c;
    cudaMalloc(&amp;amp;d_a, sz);
    cudaMalloc(&amp;amp;d_b, sz);
    cudaMalloc(&amp;amp;d_c, sz);

    cudaMemcpy(d_a, h_a, sz, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, sz, cudaMemcpyHostToDevice);

    int block_size = 256; // 每个块256个线程
    int block_cnt = (n + block_size - 1) / block_size;
    // up(n/block_size)

    kernel_add&amp;lt;&amp;lt;&amp;lt;block_cnt, block_size&amp;gt;&amp;gt;&amp;gt;(d_a, d_b, d_c, n);

    cudaMemcpy(h_c, d_c, sz, cudaMemcpyDeviceToHost);

    printf(&quot;result: \n&quot;);
    for (int i = 0; i &amp;lt; 4; i++)
        printf(&quot;%d\t+\t%d\t=\t%d\n&quot;, h_a[i], h_b[i], h_c[i]);
    printf(&quot;...\n&quot;);
    for (int i = n - 4; i &amp;lt; n; i++)
        printf(&quot;%d\t+\t%d\t=\t%d\n&quot;, h_a[i], h_b[i], h_c[i]);

    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvcc test.cu -o test
./test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result: 
0       +       0       =       0
1       +       1       =       2
2       +       2       =       4
3       +       3       =       6
...
996     +       996     =       1992
997     +       997     =       1994
998     +       998     =       1996
999     +       999     =       1998

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，似乎加上&lt;code&gt;(void **)&lt;/code&gt;强转比较好，但是我编译没有报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    cudaMalloc(&amp;amp;d_a, sz);
    cudaMalloc(&amp;amp;d_b, sz);
    cudaMalloc(&amp;amp;d_c, sz);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	cudaMalloc((void**)&amp;amp;d_a, size);
	// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;错误检查宏&lt;/h4&gt;
&lt;p&gt;如果我们把&lt;code&gt;n&lt;/code&gt;改成&lt;code&gt;10000&lt;/code&gt;，然后后面增加进程数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    int block_size = 2048; // 每个块256个线程 ，改成2048
    int block_cnt = (n + block_size - 1) / block_size;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样运行是直接错的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result: 
0       +       0       =       0
1       +       1       =       0
2       +       2       =       0
3       +       3       =       0
...
9996    +       9996    =       0
9997    +       9997    =       0
9998    +       9998    =       0
9999    +       9999    =       0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为一个block最多就1024个线程。&lt;/p&gt;
&lt;p&gt;但是在不知道这个问题的时候，可能不太好找到问题。&lt;/p&gt;
&lt;p&gt;可以加一个这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define CHECK(call) \
{ \
    const cudaError_t error = call; \
    if (error != cudaSuccess) \
    { \
        printf(&quot;Error: %s:%d, &quot;, __FILE__, __LINE__); \
        printf(&quot;code:%d, reason: %s\n&quot;, error, cudaGetErrorString(error)); \
        exit(1); \
    } \
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后代码改成下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

#define CHECK(call) \
{ \
    const cudaError_t error = call; \
    if (error != cudaSuccess) \
    { \
        printf(&quot;Error: %s:%d, &quot;, __FILE__, __LINE__); \
        printf(&quot;code:%d, reason: %s\n&quot;, error, cudaGetErrorString(error)); \
        exit(1); \
    } \
}

__global__ void kernel_add(int *d_a, int *d_b, int *d_c, int n)
{
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx &amp;lt; n)
        d_c[idx] = d_a[idx] + d_b[idx];
}

int main()
{
    int n = 10000;

    int h_a[n + 5], h_b[n + 5], h_c[n + 5];
    for (int i = 0; i &amp;lt; n; i++)
        h_a[i] = i, h_b[i] = i;

    size_t sz = n * (sizeof(int));

    int *d_a, *d_b, *d_c;
    CHECK(cudaMalloc((void**)&amp;amp;d_a, sz));
    CHECK(cudaMalloc((void**)&amp;amp;d_b, sz));
    CHECK(cudaMalloc((void**)&amp;amp;d_c, sz));

    CHECK(cudaMemcpy(d_a, h_a, sz, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_b, h_b, sz, cudaMemcpyHostToDevice));

    int block_size = 2048; // 每个块256个线程
    int block_cnt = (n + block_size - 1) / block_size;
    // up(n/block_size)

    kernel_add&amp;lt;&amp;lt;&amp;lt;block_cnt, block_size&amp;gt;&amp;gt;&amp;gt;(d_a, d_b, d_c, n);
    CHECK(cudaGetLastError());

    CHECK(cudaMemcpy(h_c, d_c, sz, cudaMemcpyDeviceToHost));

    printf(&quot;result: \n&quot;);
    for (int i = 0; i &amp;lt; 4; i++)
        printf(&quot;%d\t+\t%d\t=\t%d\n&quot;, h_a[i], h_b[i], h_c[i]);
    printf(&quot;...\n&quot;);
    for (int i = n - 4; i &amp;lt; n; i++)
        printf(&quot;%d\t+\t%d\t=\t%d\n&quot;, h_a[i], h_b[i], h_c[i]);

    CHECK(cudaFree(d_a));
    CHECK(cudaFree(d_b));
    CHECK(cudaFree(d_c));

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以看见报错了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./test
Error: test.cu:44, code:9, reason: invalid configuration argument

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;同步问题&lt;/h4&gt;
&lt;p&gt;前面说了，核函数启动是异步的。&lt;/p&gt;
&lt;p&gt;但是我并没有调用&lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt;，那么，会导致&lt;code&gt;printf&lt;/code&gt;在GPU算完之前就输出吗？&lt;/p&gt;
&lt;p&gt;不会。&lt;/p&gt;
&lt;p&gt;因为后面有个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	cudaMemcpy(h_c, d_c, sz, cudaMemcpyDeviceToHost);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个函数以同步方式执行&lt;/strong&gt;，因为在 cudaMemcpy 函数返回以及传输操作完成之前&lt;strong&gt;主机应用程序是阻塞的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在运行这个函数的时候，CUDA会先检查GPU是否在忙碌，只有前面都忙完了，才会拷贝数据。&lt;/p&gt;
</content:encoded></item><item><title>CUDA C编程入门笔记（2）</title><link>https://fuwari.vercel.app/posts/course/cuda-c/cuda-2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/cuda-c/cuda-2/</guid><pubDate>Sat, 03 Jan 2026 09:13:06 GMT</pubDate><content:encoded>&lt;h2&gt;更复杂的线程结构（2D、3D）&lt;/h2&gt;
&lt;p&gt;前面我们是这样用线程的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	int idx = threadIdx.x + blockIdx.x * blockDim.x;

	// ...

    kernel_add&amp;lt;&amp;lt;&amp;lt;block_cnt, block_size&amp;gt;&amp;gt;&amp;gt;(d_a, d_b, d_c, n);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以看见，每个线程，我们有这两个坐标变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blockIdx&lt;/code&gt;，线程块在线程格内的索引&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx&lt;/code&gt;，块内的线程索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当执行一个核函数时，CUDA运行时会自动为每个线程，分配这两个坐标变量&lt;code&gt;blockIdx&lt;/code&gt;和&lt;code&gt;threadIdx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;不过，上面的使用是一维的，实际上CUDA也支持二维、三维，这样可以方便处理2D的图像、3D的体积数据之类的。&lt;/p&gt;
&lt;p&gt;前面说的这两个坐标变量，都是基于&lt;code&gt;uint3&lt;/code&gt;定义的CUDA内置的向量类型，包含3个无符号整数。可以通过&lt;code&gt;x&lt;/code&gt;,&lt;code&gt;y&lt;/code&gt;,&lt;code&gt;z&lt;/code&gt;三个字段来指定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blockIdx.x
blockIdx.y
blockIdx.z
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;threadIdx.x
threadIdx.y
threadIdx.z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是内置的坐标变量，但是每个块有多少个线程、整个GPU有多少块，是由&lt;code&gt;blockDim&lt;/code&gt;、&lt;code&gt;gridDim&lt;/code&gt;来决定的。同理，也有&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blockDim.x  // 线程块的维度（每个块有多少线程）
blockDim.y
blockDim.z
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;gridDim.x   // 线程格的维度（有多少个块）
gridDim.y
gridDim.z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎么使用呢，大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1D的写法
int block_size = 256;
int grid_size = (n + block_size - 1) / block_size;
kernel&amp;lt;&amp;lt;&amp;lt;grid_size, block_size&amp;gt;&amp;gt;&amp;gt;(...);

// 2D的写法
dim3 block(16, 16);      // 每个块 16×16 = 256 个线程
dim3 grid(32, 32);       // 32×32 = 1024 个块
kernel&amp;lt;&amp;lt;&amp;lt;grid, block&amp;gt;&amp;gt;&amp;gt;(...);

// 3D的写法
dim3 block(8, 8, 4);     // 8×8×4 = 256 个线程
dim3 grid(10, 10, 10);   // 10×10×10 = 1000 个块
kernel&amp;lt;&amp;lt;&amp;lt;grid, block&amp;gt;&amp;gt;&amp;gt;(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;未使用的维度默认为1。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;线程格和线程块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GPU
 └─ Grid 线程格
     ├─ Block 0 线程块
     │   ├─ Thread 0
     │   ├─ Thread 1
     │   └─ Thread 2
     │
     ├─ Block 1 线程
     │   ├─ Thread 0
     │   ├─ Thread 1
     │   └─ Thread 2
     │
     └─ Block 2 线程块
         └─ ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然都可以用3D标号，但是有硬件限制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Grid的最大维度（不同GPU可能不同）
gridDim.x: 最大 2^31 - 1
gridDim.y: 最大 65535
gridDim.z: 最大 65535

// Block的最大维度
blockDim.x: 最大 1024
blockDim.y: 最大 1024  
blockDim.z: 最大 64
// 且 blockDim.x × blockDim.y × blockDim.z ≤ 1024
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;
&lt;p&gt;一个线程格，不代表一个GPU。&lt;/p&gt;
&lt;p&gt;应该说，一个线程格 = 一次kernel调用创建的所有线程。&lt;/p&gt;
&lt;p&gt;GPU是硬件，是物理存在的显卡。Grid是软件概念，每次调用kernel，就创建一个grid。一个GPU可以同时运行多个Grid（如果资源够），Grid运行完就消失了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数据内存布局&lt;/h3&gt;
&lt;p&gt;虽然线程数是可以用2D 3D的（逻辑上），但是显存是1D的（物理意义上）。&lt;/p&gt;
&lt;p&gt;在前面我们提到过这个&lt;code&gt;cudaMalloc&lt;/code&gt;函数，用于GPU分配空间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cudaError_t cudaMalloc (void** devPtr, size_t size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个是在GPU显存上，申请一个&lt;code&gt;size&lt;/code&gt;字节的连续内存，返回的是是否分配成功，然后分配的GPU显存的指针，会被存在&lt;code&gt;devPtr&lt;/code&gt;指向的地址中。&lt;/p&gt;
&lt;p&gt;这里分配的显存，是线性的，也就是1D的。&lt;/p&gt;
&lt;h2&gt;进一步的核函数&lt;/h2&gt;
&lt;h3&gt;函数限定符&lt;/h3&gt;
&lt;p&gt;前面我们用&lt;code&gt;__global__&lt;/code&gt;来定义了和函数。CUDA C中还有更多的函数限定符。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;限定符&lt;/th&gt;
&lt;th&gt;在哪里执行&lt;/th&gt;
&lt;th&gt;谁能调用它&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__global__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;CPU（或GPU，计算能力≥3.0）&lt;/td&gt;
&lt;td&gt;必须返回void，这就是&quot;核函数&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__device__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;只能GPU调用&lt;/td&gt;
&lt;td&gt;GPU内部的辅助函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__host__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;只能CPU调用&lt;/td&gt;
&lt;td&gt;可省略，普通C函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;__device__&lt;/code&gt; 和 &lt;code&gt;__host__&lt;/code&gt; 可以同时使用，让同一个函数在CPU和GPU上都能用。&lt;/p&gt;
&lt;h2&gt;CUDA执行模型&lt;/h2&gt;
&lt;h4&gt;SM：流式处理器&lt;/h4&gt;
&lt;p&gt;前面说了，当CPU调用一次核函数，比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;train_kernel&amp;lt;&amp;lt;&amp;lt;grid_size, block_size&amp;gt;&amp;gt;&amp;gt;(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候，我们要调用非常非常多的线程。而在调用时，&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;grid, block&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; 是一个软件代码里划分的层次，这里我们可以用二维或者三维来组织这些线程。&lt;/p&gt;
&lt;p&gt;不过，在扔进显卡后，这些线程实际上还都是一维的，那么这时候是怎么对应的呢？&lt;/p&gt;
&lt;p&gt;实际上，我们用一个Grid（线程格）来组织这么多的线程。而其中，一个Block（线程块）会被分配到一个指定的SM。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：是一个Block会被分配给一个SM，但一个SM可以容纳不止一个Block。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个SM有这样的一些东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CUDA核心&lt;/li&gt;
&lt;li&gt;共享内存、一级缓存（这里共享内存，是每个block占用一份，不是SM上所有Block共享的。所谓共享，是同一个Block内的线程共享。）&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;/ol&gt;
&lt;p&gt;一个家用级别的显卡，比如RTX3060，大概有28个SM。而例如A100这种专业的显卡，也只有108个SM左右。&lt;/p&gt;
&lt;p&gt;那么，一个任务，会分配给几个SM呢？&lt;/p&gt;
&lt;p&gt;先来看硬件上限。一个Block最多占用一个SM，不会跨SM，所以Grid有多少个Block，就最多能用多少个SM（当然也不能超过GPU的SM总数）。&lt;/p&gt;
&lt;p&gt;这是上限，那实际呢？每个SM资源有限，主要是三样东西：寄存器数量、共享内存大小、以及 SM 支持的最大线程数。一个 Block 进来会占用这些资源，如果一个 SM 的资源还够，就可以再塞第二个 Block 进来同时跑，这叫做&lt;strong&gt;占用率（Occupancy）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以实际用几个 SM，是由“Block 数量”和“每个 SM 能同时容纳几个 Block”
共同决定的，硬件调度器自动分配，人是控制不了具体哪个 Block 去哪个 SM。&lt;/p&gt;
&lt;p&gt;如果都分配完了呢？&lt;/p&gt;
&lt;p&gt;假设一个SM有 48KB 共享内存，每个 Block 申请 20KB，那这个 SM 只能同时容纳 2 个 Block（40KB），第 3 个 Block 塞不进来，就要等前面的 Block 执行完释放资源，才能进来。&lt;/p&gt;
&lt;p&gt;如果所有 SM 都满了，剩下的 Block 就排队等待。&lt;/p&gt;
&lt;p&gt;而且共享内存只是卡住的原因之一，寄存器也会卡。（比如超级无敌多的局部变量！）&lt;/p&gt;
&lt;h4&gt;SIMT架构、warp&lt;/h4&gt;
&lt;p&gt;CUDA采用的是SIMT，也就是单指令多线程架构。和这个相对的是SIMD，单指令多数据架构。&lt;/p&gt;
&lt;p&gt;这看上去比较难懂，对比一下可能比较好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SIMD（Single Instruction, Multiple Data）&lt;/strong&gt; 是 CPU 上的概念。一条指令，同时对多个数据做同样的操作。比如一条 &lt;code&gt;AVX&lt;/code&gt; 指令可以同时加 8 个 float。但这 8 个数据必须打包在一起，用专门的寄存器，程序员（或编译器）要显式地去安排这件事。&lt;/p&gt;
&lt;p&gt;举个例子，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 8; i++) {
    c[i] = a[i] + b[i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这看上去是循环加了 8 次，但编译器开了 &lt;code&gt;-O2&lt;/code&gt; 优化之后，可能直接生成一条 &lt;code&gt;AVX&lt;/code&gt; 指令，把 8 个 float 一次加完。没写任何特殊代码，但 SIMD 已经在用了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SIMT（Single Instruction, Multiple Threads）&lt;/strong&gt; 是 GPU 的概念。同样是一条指令操作多份数据，但包装方式不同——它伪装成&quot;多个独立线程&quot;，每个线程有自己独立的寄存器、自己的 PC 指针，你写代码的时候感觉像在写普通的单线程逻辑，硬件偷偷把 32 个线程捆成一个 Warp 一起跑。&lt;/p&gt;
&lt;p&gt;还是举个例子，要对32个数各自+1，核函数是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__global__ void add_one(int *a) {
    int i = threadIdx.x;
    a[i] = a[i] + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里写的是 第 i 个线程处理第 i 个数，每个线程是独立的逻辑，也就是单线程视角的代码。&lt;/p&gt;
&lt;p&gt;但硬件实际干的是，把 Thread 0 到 Thread 31 捆成一个 Warp，同时对 32 个数据执行同一条加法指令。本质上和 SIMD 做的事一模一样。&lt;/p&gt;
&lt;p&gt;但是这样就很显然，有一个问题——如果这32个线程，我写的不是顺序执行呢？比如中间有&lt;code&gt;if&lt;/code&gt;，那他们有可能同时无法执行同样的指令。&lt;/p&gt;
&lt;p&gt;这就是&lt;strong&gt;线程束分化（Warp Divergence）&lt;/strong&gt;，这是 SIMT 的核心痛点。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__global__ void kernel(int *a) {
    int i = threadIdx.x;
    if (i % 2 == 0) {
        a[i] = a[i] + 1;  // 偶数线程走这里
    } else {
        a[i] = a[i] * 2;  // 奇数线程走这里
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一个 Warp 里，Thread 0、2、4... 要执行加法，Thread 1、3、5... 要执行乘法。但硬件要求 32 个线程同时执行同一条指令，怎么办？&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;如果我们再极端一点，假设一共有 $n$ 个分叉，总共有 $2^n$ 种可能的指令，就在最坏情况下，需要 $O(2^n)$ 个时间周期。不过这是理论，因为一个warp只有32个线程，不可能有超过 32 条不同的路径同时出现。所以最坏情况不是  $O(2^n)$ ，而是 $O(\min(2^n, 32))$ ，也就是最坏情况是 32 步串行执行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;当 $n$ 比较小的时候（比如 $n=1$ 就是 2 条路径，$n=2$ 就是 4 条），$2^n$ 是瓶颈。但 n 一旦超过 5，$2^n$ 就超过 32 了，后面再多分支也不会更坏，因为线程数就这么多。&lt;/p&gt;
&lt;p&gt;所以现实中的最坏情况是：Warp 里 32 个线程每个人走的路径都不一样，硬件要串行执行 32 次，每次只有 1 个线程在干活，其余 31 个闲置。效率变成 1/32，退化成完全串行了。&lt;/p&gt;
&lt;p&gt;这也是为什么 GPU 并不适合分支密集、逻辑复杂的任务——这类任务交给 CPU 反而更合适。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在并行线程中，可能多个线程都要访问同一个数据，或者写入同一个数据。假设这是共享内存，那怎么办？&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__global__ void kernel(int *a) {
    a[0] = threadIdx.x;  // 32个线程都往a[0]写
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;32 个线程同时写同一个地址，最终 a[0] 是多少？&lt;/p&gt;
&lt;p&gt;不确定，取决于谁最后写进去，结果是随机的。&lt;/p&gt;
&lt;p&gt;如果确实需要这么做的话，可以用原子操作。比如求和，可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;atomicAdd(&amp;amp;result, value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&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;/p&gt;
&lt;p&gt;比如求和这个问题，真的需要32个数的和的话，怎么办呢？&lt;/p&gt;
&lt;p&gt;朴素的写法就是顺序一个个加，需要32个时钟周期。但是我们毕竟有32个线程。我们可以在第一个时钟周期，1和2、3和4、5和6……这样两两线程负责相加。&lt;/p&gt;
&lt;p&gt;第一轮加16次，假设都把各自的和存在奇数线程的存储位，比如&lt;code&gt;a[1],a[3],a[5]...&lt;/code&gt;，然后第二轮，再同理，再各自两两相加，1和3，5和7……这样，最后一轮就在&lt;code&gt;a[1]&lt;/code&gt;得到结果。&lt;/p&gt;
&lt;p&gt;总共需要 $\log 32=5$ 个时钟周期就可以了。&lt;/p&gt;
&lt;p&gt;当然，这里涉及到一个技巧：我们一般不是这样奇偶交错相加的，我们一般会让1去加9，2去加10……8去加16。&lt;/p&gt;
&lt;p&gt;为什么？&lt;/p&gt;
&lt;p&gt;假设我们不是32个数相加，是128个数相加，这样占据了4个warp。&lt;/p&gt;
&lt;p&gt;如果是奇偶相加，第一轮，我们所有warp都要干活。&lt;/p&gt;
&lt;p&gt;如果是我们说的第二种做法——前一半去加后一半——那第一轮，GPU调度器就会聪明地发现，后面2个warp，完全不干活了，可以提前下班了！释放资源。&lt;/p&gt;
&lt;p&gt;除此之外，连续内存读取，是连续的东西，也涉及合并内存访问，比随机访问更快。&lt;/p&gt;
&lt;h5&gt;循环展开&lt;/h5&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int offset = 4; offset &amp;gt; 0; offset /= 2) {
    partial += offset;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个代码写出来后，这是循环。&lt;/p&gt;
&lt;p&gt;但是我们知道，循环是比顺序结构更慢的。我们会尽可能地少用循环，比如把两个&lt;code&gt;for&lt;/code&gt;合并成一个，或者干脆展开。&lt;/p&gt;
&lt;p&gt;上面的代码就可以展开，我们很容易注意到，循环次数不是固定的吗？&lt;/p&gt;
&lt;p&gt;直接展开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;partial += 4;
partial += 2;
partial += 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们除了手动展开，也可以用&lt;code&gt;#pragma unroll&lt;/code&gt;这个指令，让nvcc编译器，自动在编译的时候就在底层汇编里面展开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma unroll
for (int offset = 4; offset &amp;gt; 0; offset /= 2) {
    partial += offset;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是有一个问题啊——参考前面的求和代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 雇佣了 512 个线程
int tid = threadIdx.x; 

// 第一轮循环：步长是 256 (512的一半)
for (int stride = 256; stride &amp;gt; 0; stride /= 2) {
    if (tid &amp;lt; stride) {  // 只有编号小于 256 的线程才准干活！
        idata[tid] += idata[tid + stride];
    }
    __syncthreads(); // 所有人必须等干活的人做完
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我们知道，没用的线程是可以释放啊。但是我们有一个&lt;code&gt;__syncthreads()&lt;/code&gt;，也就意味着强制同步，即使上面那个 if 导致后一半的线程没进括号干活，它们也必须在这里死等前一半的人加完，才能进入下一轮循环。&lt;/p&gt;
&lt;p&gt;既然我们知道，第一轮后面一半都在摸鱼，那直接第一步就开除了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 我们现在只雇佣 256 个线程了！网格大小直接砍半。
int tid = threadIdx.x; 

// 在进入循环前，让 0 号线程去把 0 号和 256 号加起来；
// 让 1 号线程去把 1 号和 257 号加起来...
g_idata[idx] += g_idata[idx + blockDim.x]; 

__syncthreads();

// 此时，512 个数字已经被折叠成 256 个了。
// 接下来再舒舒服服地进入原先的循环。
for (int stride = 128; stride &amp;gt; 0; stride /= 2) { 
    // ... 原来的逻辑 ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;隐式同步&lt;/h5&gt;
&lt;p&gt;同样是上面的，假设，随着循环不断进行，我们数据 $\leq 32$ 了！&lt;/p&gt;
&lt;p&gt;这时候，&lt;strong&gt;全场只有编号 0 到 31 的这 32 个线程在干活&lt;/strong&gt;了。&lt;/p&gt;
&lt;p&gt;这 32 个线程，刚好组成了一个 Warp（线程束）。 GPU 硬件设计里有一个铁律（SIMT 架构）：同一个 Warp 里的 32 个线程，在物理电路上是绑死在一起的！它们就像一条龙舟上的 32 个桨手，必须在同一个微秒、听同一个鼓点、挥动同一次船桨。&lt;/p&gt;
&lt;p&gt;这意味着，线程 0 和线程 8 在执行上一轮的加法和写入时，&lt;strong&gt;是绝对在同一个时钟周期内完成的&lt;/strong&gt;。（也就是说，根本不需要&lt;code&gt;__syncthreads&lt;/code&gt;了！）&lt;/p&gt;
&lt;p&gt;甚至可以直接删掉&lt;code&gt;for&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只让前 32 个线程进来
if (tid &amp;lt; 32) {
    volatile int *vmem = idata; 
    
    // 以下 6 行代码，没有 for 循环，没有 __syncthreads() 互相等待
    // 32 个线程靠着天然的硬件同步，像机关枪一样一口气打完！
    vmem[tid] += vmem[tid + 32]; // 这是应对如果刚好剩 64 个数据的情况
    vmem[tid] += vmem[tid + 16];
    vmem[tid] += vmem[tid + 8];
    vmem[tid] += vmem[tid + 4];
    vmem[tid] += vmem[tid + 2];
    vmem[tid] += vmem[tid + 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很容易注意到，这里有一个&lt;code&gt;volatile&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;虽然 32 个线程动作是一致的，但现代编译器（&lt;code&gt;nvcc&lt;/code&gt;）会优化，发现连续对 &lt;code&gt;vmem[tid]&lt;/code&gt; 做 6 次加法，它会偷偷把中间结果存在各自私有的寄存器，然后一次性写回。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volatile&lt;/code&gt;就是强制要求，必须进内存的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;关于前面的，“根本不需要 &lt;code&gt;__syncthreads()&lt;/code&gt; 了！甚至可以直接删掉 for... 32 个线程靠着天然的硬件同步，像机关枪一样一口气打完！”&lt;/p&gt;
&lt;p&gt;这里还有一个问题。在2017年前，只是对的，但是在后面的GPU上不一定对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;过去的架构（Pascal及更早）：&lt;/strong&gt; 同一个Warp的32个线程共享同一个程序计数器（PC）。它们确实是绝对同步的（隐式同步），很多老旧的CUDA教程会教大家省略同步函数，直接用 &lt;code&gt;volatile&lt;/code&gt; 来榨干性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现代架构（Volta及之后，如RTX 20/30/40系列）：&lt;/strong&gt; NVIDIA引入了&lt;strong&gt;独立线程调度（Independent Thread Scheduling）&lt;/strong&gt;。这意味着同一个Warp中的每个线程现在都有自己的程序计数器和调用栈。虽然硬件依然倾向于让它们一起执行，但系统&lt;strong&gt;不再保证&lt;/strong&gt;它们绝对同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正确的做法：&lt;/strong&gt; 从CUDA 9.0开始，如果需要Warp内的线程进行同步，&lt;strong&gt;必须显式调用&lt;/strong&gt; &lt;code&gt;__syncwarp()&lt;/code&gt;。在现代CUDA编程中，处理这种Warp级别的数据归约（Reduction），官方推荐使用 Warp Shuffle 指令（如 &lt;code&gt;__shfl_down_sync&lt;/code&gt;），它甚至不需要通过共享内存或全局内存就能在寄存器之间直接交换数据，速度极快且完全安全。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>CUDA C编程入门笔记（3）</title><link>https://fuwari.vercel.app/posts/course/cuda-c/cuda-3/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/cuda-c/cuda-3/</guid><pubDate>Sat, 03 Jan 2026 09:13:05 GMT</pubDate><content:encoded>&lt;h2&gt;内存类型&lt;/h2&gt;
&lt;p&gt;前面我们已经学过了，用&lt;code&gt;cudaMalloc&lt;/code&gt;分配全局显存。&lt;/p&gt;
&lt;p&gt;内存还有几个类型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;内存类型&lt;/th&gt;
&lt;th&gt;在哪里&lt;/th&gt;
&lt;th&gt;谁能访问&lt;/th&gt;
&lt;th&gt;速度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;寄存器&lt;/td&gt;
&lt;td&gt;SM片上&lt;/td&gt;
&lt;td&gt;单个线程私有&lt;/td&gt;
&lt;td&gt;最快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;共享内存&lt;/td&gt;
&lt;td&gt;SM片上&lt;/td&gt;
&lt;td&gt;同一Block内所有线程&lt;/td&gt;
&lt;td&gt;很快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本地内存&lt;/td&gt;
&lt;td&gt;显存（全局内存区域）&lt;/td&gt;
&lt;td&gt;单个线程私有&lt;/td&gt;
&lt;td&gt;慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全局内存&lt;/td&gt;
&lt;td&gt;显存&lt;/td&gt;
&lt;td&gt;所有线程&lt;/td&gt;
&lt;td&gt;慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常量内存&lt;/td&gt;
&lt;td&gt;显存（有缓存）&lt;/td&gt;
&lt;td&gt;所有线程只读&lt;/td&gt;
&lt;td&gt;较快&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;本地内存&lt;/h3&gt;
&lt;p&gt;全局内存不用说了，前面动态分配的就是。下面是本地内存。&lt;/p&gt;
&lt;p&gt;本地内存逻辑上属于某个线程私有，实际上和全局内存一样慢。什么是本地内存？简单讲，就是单个线程的一些变量，在寄存器里放不下了，就会用到本地内存。&lt;/p&gt;
&lt;p&gt;（写作本地内存，逻辑上我们当成单个线程私有的，其实就是给物理上扔到显存里，读取很慢）&lt;/p&gt;
&lt;h3&gt;共享内存&lt;/h3&gt;
&lt;p&gt;共享内存是用&lt;code&gt;__shared__&lt;/code&gt;修饰的，类似CPU一级缓存，但是不同的是，是可以自己开辟空间的。也就是说，我可以直接指定在共享内存中开一个数组用来存东西，同一个Block中读取速度比较快。&lt;/p&gt;
&lt;h3&gt;常量内存&lt;/h3&gt;
&lt;p&gt;我们知道，全局内存虽然大，但是很慢啊。但是如果我们要给所有线程一块数据，只读不写，有没有办法加速？&lt;/p&gt;
&lt;p&gt;常量内存。&lt;/p&gt;
&lt;p&gt;常量内存用&lt;code&gt;__constant__&lt;/code&gt;修饰，声明在核函数外面，也就是全局作用域。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__constant__ float filter[16];  // 声明常量内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道，正常的C语言，写常量是直接在声明里写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int a = 16;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，这里声明的是在显存里的，就不能直接像C语言一样直接写。&lt;/p&gt;
&lt;p&gt;所以要用专门的函数写入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cudaMemcpyToSymbol(filter, h_filter, 16 * sizeof(float));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这写完后，在kernel执行期间，GPU只能读不能写。&lt;/p&gt;
&lt;p&gt;这有什么好处呢？&lt;/p&gt;
&lt;p&gt;快。不过这里的快是有要求的。GPU有专门的常量缓存。如果同一个Warp里的32个线程，同时读的是同一个地址，那只需要读一次，广播给所有线程，速度接近寄存器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;我们可能会想，在用CUDA C的时候，诸如神经网络的层数&lt;code&gt;layer_size&lt;/code&gt;之类的，从前我们会这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;train_kernel_skipgram&amp;lt;&amp;lt;&amp;lt;grid, block&amp;gt;&amp;gt;&amp;gt;(
    d_syn0, d_syn1neg, d_batch, 
    BATCH_SIZE,
    layer1_size,      // ← 这些标量参数
    alpha
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了常量内存后，直接放常量内存，会不会更快？&lt;/p&gt;
&lt;p&gt;不会。&lt;/p&gt;
&lt;p&gt;因为&lt;strong&gt;CUDA的核函数参数，本来就自动存在常量内存里&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那什么时候才值得手动用 &lt;code&gt;__constant__&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;假设我们有一个什么东西要查表，就可以这样扔进常量内存里。&lt;/p&gt;
&lt;p&gt;而且，常量内存要求的比较苛刻，如果是随机采样的访问查表，也不优，反而可能比全局内存更慢（常量内存不擅长处理分散读取）。&lt;/p&gt;
&lt;p&gt;所以常量内存的使用条件其实挺苛刻的，同时满足三点才划算：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只读&lt;/li&gt;
&lt;li&gt;数据小于64KB（常量内存只有这么大）&lt;/li&gt;
&lt;li&gt;访问模式是&quot;所有线程读同一地址&quot;（广播）&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;全局内存&lt;/h3&gt;
&lt;p&gt;前面我们学过可以动态分配显存，那可以静态分配吗？&lt;/p&gt;
&lt;p&gt;可以。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;__device__&lt;/code&gt; 可以用来声明变量，这样声明的变量住在&lt;strong&gt;全局内存&lt;/strong&gt;里，但是是静态分配的（编译时就确定大小）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__device__ float d_alpha;           // 单个标量
__device__ float d_table[1000];     // 固定大小的数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从CPU读写 &lt;code&gt;__device__&lt;/code&gt; 变量，不能直接赋值，要用专门的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__device__ float d_alpha = 0.025f;

// CPU往里写
float val = 0.01f;
cudaMemcpyToSymbol(d_alpha, &amp;amp;val, sizeof(float));

// CPU从里读
float result;
cudaMemcpyFromSymbol(&amp;amp;result, d_alpha, sizeof(float));
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;但实际上这个写法，很少用。&lt;/p&gt;
&lt;p&gt;因为访问速度和 &lt;code&gt;cudaMalloc&lt;/code&gt; 分配的一样，都是全局内存，没有任何性能优势。而且大小编译时就要确定，不灵活。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内存的加速问题&lt;/h2&gt;
&lt;h3&gt;固定内存&lt;/h3&gt;
&lt;p&gt;我们学操作系统的时候就知道，操作系统为了管理内存，普通内存都是可以换页的。&lt;/p&gt;
&lt;p&gt;当内存紧张时，OS可以把这块数据临时挪到硬盘上（swap），腾出物理内存给别的程序用。&lt;/p&gt;
&lt;p&gt;如果需要大量的GPU向CPU传输，可以改成固定内存。也就是用前面的&lt;code&gt;cudaMallocHost&lt;/code&gt;来分配内存。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *corpus = (int *)malloc(corpus_size * sizeof(int));

// 改成
int *corpus;
cudaMallocHost(&amp;amp;corpus, corpus_size * sizeof(int));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;零拷贝内存&lt;/h3&gt;
&lt;p&gt;普通的数据传输流程是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CPU内存 → cudaMemcpy → GPU显存 → 核函数使用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;cudaMemcpy&lt;/code&gt;需要显式传输，占用时间。&lt;/p&gt;
&lt;p&gt;零拷贝内存的思路是：&lt;strong&gt;跳过这个传输步骤，让GPU直接读CPU内存&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float *h_data;
cudaHostAlloc(&amp;amp;h_data, size, cudaHostAllocMapped);  // 分配零拷贝内存

float *d_data;
cudaHostGetDevicePointer(&amp;amp;d_data, h_data, 0);  // 获取GPU视角的指针

// 核函数直接用d_data，不需要cudaMemcpy
kernel&amp;lt;&amp;lt;&amp;lt;grid, block&amp;gt;&amp;gt;&amp;gt;(d_data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，这里有个问题，零拷贝的意思是不需要显式调用cudaMemcpy，但数据还是要通过PCI-E总线传输的，这个物理过程跑不掉。&lt;/p&gt;
&lt;p&gt;众所周知，PCI-E总线很慢，比GPU显存带宽慢，所以其实性能上是不优的。&lt;/p&gt;
&lt;p&gt;什么时候用呢？&lt;/p&gt;
&lt;p&gt;适合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据量太大，GPU显存根本放不下&lt;/li&gt;
&lt;li&gt;数据只读一遍，没必要先传过去再读&lt;/li&gt;
&lt;li&gt;CPU和GPU需要共享同一块数据，频繁同步&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;内存访问模式&lt;/h2&gt;
&lt;p&gt;当一个指令要取内存里的数据的时候，我们有成千上万个指令在并发。难道一个个排队取吗？&lt;/p&gt;
&lt;p&gt;不是的。&lt;/p&gt;
&lt;p&gt;GPU访问全局内存，不是一个线程一个线程取，是一次取一大块。&lt;/p&gt;
&lt;p&gt;简单来说，当一个warp（32个线程）要访问全局内存的时候，硬件会把这些访问合并起来，合并成尽量少点内存事务，每次事务取32字节、64字节或者128字节。&lt;/p&gt;
&lt;p&gt;取的次数越少，速度越快。&lt;/p&gt;
&lt;h3&gt;合并访问、非合并访问&lt;/h3&gt;
&lt;p&gt;最理想的情况，是这样的：32个线程，访问连续的32个float（每个4字节，共128字节），硬件一次事务就全取完了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设 blockDim.x = 32，一个warp
int i = threadIdx.x;
float val = data[i];  // 线程0取data[0]，线程1取data[1]...线程31取data[31]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程:  0  1  2  3  4  5 ... 31
地址: &lt;code&gt;[0][1][2][3][4][5]...[31]&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;pre&gt;&lt;code&gt;int i = threadIdx.x;
float val = data[i * 2];  // 线程0取[0]，线程1取[2]，线程2取[4]...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这取的地址是&lt;code&gt;[0]  [2]  [4]  [6] ...&lt;/code&gt;，有间隔啊！&lt;/p&gt;
&lt;p&gt;每隔一个元素，硬件没法合并（不能说完全无法合并，但是势必要浪费一半），需要多次事务，带宽浪费一半。&lt;/p&gt;
&lt;p&gt;更坏的是随机访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float val = data[random_index[i]];  // 每个线程访问随机地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;32个线程触发32次独立事务，比合并访问慢32倍。&lt;/p&gt;
&lt;h3&gt;对齐访问&lt;/h3&gt;
&lt;p&gt;先从CPU讲起。假设有一个结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Example {
    char a;   // 1字节
    int b;    // 4字节
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个结构体占多少个字节？&lt;/p&gt;
&lt;p&gt;看上去是5个字节是吗？但并不是，&lt;code&gt;sizeof(Example)&lt;/code&gt;其实是8。&lt;/p&gt;
&lt;p&gt;因为CPU访问内存时，硬件要求（或强烈偏好）一个N字节的数据类型，其起始地址必须是N的倍数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt;是4字节，所以它的地址必须是4的倍数。编译器会在&lt;code&gt;a&lt;/code&gt;后面自动插入3字节的padding，让&lt;code&gt;b&lt;/code&gt;对齐到偏移量4的位置。&lt;/p&gt;
&lt;p&gt;（至于结构体的对齐要求，是其成员中最大的那个对齐要求。）&lt;/p&gt;
&lt;p&gt;一般来说，在我们写普通C代码的时候，编译器会自动插入padding，自动把变量对齐。&lt;/p&gt;
&lt;p&gt;如果要看见不对齐的效果，可以尝试用指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    char buf[8];
    int *p = (int *)(buf + 1);  // 故意让int指针指向一个非4对齐的地址
    *p = 42;
    printf(&quot;%d\n&quot;, *p);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在x86上这段代码能跑，输出42，但访问速度会比对齐的慢——因为一次内存读取跨越了两个cache line边界，硬件需要读两次再拼起来。&lt;/p&gt;
&lt;p&gt;在某些ARM芯片上（比如早期的嵌入式ARM），这段代码会直接触发SIGBUS崩溃。现代ARM（比如手机上的）一般也改成不崩但变慢了。&lt;/p&gt;
&lt;p&gt;然后再看GPU上的对齐。&lt;/p&gt;
&lt;p&gt;我们前面知道了，GPU一次内存事务是32字节、64字节或128字节，而且这个事务的起始地址必须是事务大小的整数倍。&lt;/p&gt;
&lt;p&gt;比如一个warp的32个线程，每人读一个float（4字节），总共128字节。如果起始地址是128的倍数，硬件一次事务搞定。如果起始地址不对齐，比如偏移了4字节，那这128字节横跨了两个128字节的事务边界，硬件就要发两次事务，带宽浪费接近一半。&lt;/p&gt;
&lt;h2&gt;线程束洗牌指令&lt;/h2&gt;
&lt;p&gt;如果两个线程交换数据，在kepler架构之前，必须要通过共享内存。一个写入，一个读。&lt;/p&gt;
&lt;p&gt;就不能直接读吗？&lt;/p&gt;
&lt;p&gt;可以。&lt;/p&gt;
&lt;p&gt;洗牌指令（Shuffle Instruction）：&lt;strong&gt;它允许同一个 Warp 内的线程，直接读取对方的寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;既然要读别人的寄存器，总得知道别人是谁。这就涉及到了一个东西——&lt;strong&gt;束内线程（Lane）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个 Warp 永远是 32 个线程，这就像一排有 32 个座位的连椅。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;threadIdx.x&lt;/code&gt; 是你在整个大教室（Block）里的绝对编号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;laneID&lt;/code&gt;（束内线程索引）&lt;/strong&gt; 就是你在这排 32 个连椅上的“相对座位号”，范围永远是 &lt;code&gt;[0, 31]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一维的线程块中可以这样计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;laneID = threadIdx.x % 32;
warpID = threadIdx.x / 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;不同洗牌指令&lt;/h3&gt;
&lt;h4&gt;广播机制：&lt;code&gt;__shfl&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;__shfl(val, 2)&lt;/code&gt;，就是所有人都把 &lt;code&gt;laneID=2&lt;/code&gt; 的那个人的 &lt;code&gt;val&lt;/code&gt; 复制到自己这里。&lt;/p&gt;
&lt;h4&gt;向上看齐：&lt;code&gt;__shfl_up&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;每个人都去读比自己座位号&lt;strong&gt;小&lt;/strong&gt; &lt;code&gt;delta&lt;/code&gt; 的人的数据。&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;__shfl_up(val, 2)&lt;/code&gt;，也就是去找左边隔一个座位的人拿数据（如图 5-21 所示，箭头向右指，数据向右移）。最左边没有人的那些线程，数据保持不变。&lt;/p&gt;
&lt;h4&gt;向下看齐：&lt;code&gt;__shfl_down&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;每个人去读比自己座位号&lt;strong&gt;大&lt;/strong&gt; &lt;code&gt;delta&lt;/code&gt; 的人的数据。比如 &lt;code&gt;__shfl_down(val, 2)&lt;/code&gt;，去找右边隔一个座位的人拿数据，数据整体向左移（如图 5-22 所示）。&lt;strong&gt;这个指令是你 Word2Vec 代码里用的！&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;蝴蝶交换：&lt;code&gt;__shfl_xor&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;通过按位异或（XOR）来配对交换数据。这个模式非常适合 FFT（快速傅里叶变换）这种交叉运算。&lt;/p&gt;
</content:encoded></item><item><title>厦门大学计算机《软件工程》专业课速通笔记（1）</title><link>https://fuwari.vercel.app/posts/course/software-engineering/se-notes-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/software-engineering/se-notes-1/</guid><pubDate>Tue, 30 Dec 2025 04:46:36 GMT</pubDate><content:encoded>&lt;p&gt;这门是厦门大学计科大四上的专业课，一学期一点都没学，全在玩了（保研完了是这样的）。&lt;/p&gt;
&lt;p&gt;下面是本人用时一天的速通笔记。（基本上直接面向做题）&lt;/p&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;2  项目管理&lt;/h2&gt;
&lt;p&gt;在估算软件项目规模的适合，直接方法是代码行数LOC，间接方法是功能点FP。&lt;/p&gt;
&lt;h3&gt;LOC&lt;/h3&gt;
&lt;p&gt;LOC，Lines of Code，代码行数，用来估计软件项目的规模大小。&lt;/p&gt;
&lt;p&gt;一般每个模块有三个估计值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;乐观值&lt;/strong&gt;（最理想情况）：4,600行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可能值&lt;/strong&gt;（最可能情况）：6,900行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;悲观值&lt;/strong&gt;（最坏情况）：8,600行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;估算期望值的公式
$$
S=\frac{S_{乐观}+4\times S_{可能}+S_{悲观}}{6}
$$&lt;/p&gt;
&lt;h3&gt;FP&lt;/h3&gt;
&lt;p&gt;FP，Funcition Point，功能点。这里有几个概念：FP, UFP, AFP&lt;/p&gt;
&lt;p&gt;FP是功能点，包括UFP（未调整功能点，Unadjusted Function Point）和AFP（调整后功能点，Adjusted Function Point）&lt;/p&gt;
&lt;p&gt;UFP，考虑功能数量、功能本身的复杂度（简单还是复杂）：
$$
UFP=\sum(功能数量\times 权重)
$$
一般来说，会考虑外部输入EI、外部输出EO、外部查询EQ、内部逻辑文件ILF、外部接口文件EIF，这五个部分。&lt;/p&gt;
&lt;p&gt;通常的权重表如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229234910892.png&quot; alt=&quot;image-20251229234910892&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;AFP，加上了系统技术难度的考虑。&lt;/p&gt;
&lt;p&gt;如何加上技术难度的考虑呢？引入一个 $TCF$ 作为技术复杂度调整系数公式
$$
TCF=0.65+0.01\times\sum(复杂度因子评分)
$$
TCF一般考虑14个复杂度因子：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229235725662.png&quot; alt=&quot;image-20251229235725662&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;然后最后调整后的AFP
$$
AFP=UFP\times TCF
$$&lt;/p&gt;
&lt;p&gt;一般还要计算一个人月，生产率一般是 10~20 FP/人月
$$
人月=AFP\div 生产率
$$&lt;/p&gt;
&lt;p&gt;**题1：**假设某个项目的功能点统计如下。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230000440303.png&quot; alt=&quot;image-20251230000440303&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;且技术复杂度因子评分综合为30，团队的生产率为 15FP/人月。&lt;/p&gt;
&lt;p&gt;计算项目的人月。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;
$$
UFP=10\times 4+5\times 5+8\times 4+4\times 10+2\times 7=151\
TCF=0.65+0.01\times 30=0.95\
AFP=TCF\times UFP=143.45\
人月=AFP\div 15=9.563 人月
$$&lt;/p&gt;
&lt;h3&gt;挣值分析&lt;/h3&gt;
&lt;p&gt;定量估算项目进度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BAC&lt;/strong&gt;，Buget At Completion，总预算。（比如，总预算，582人日）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BCWS&lt;/strong&gt;，Budgeted Cost of Work Scheduled，预算工作成本。也就是说，到今天为止，应该完成的工作量，按预算价格算。（比如，预计完成15个任务，&quot;计划工作量&quot;之和 = 156.5人日）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BCWP&lt;/strong&gt;，Budgeted Cost of Work Performed, 实际工作值的成本，也就是说，到今天为止，实际上完成的工作量，也是按预期算。（实际值完成12个任务，用计划工作量，完成了126.5人日）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ACWP&lt;/strong&gt;，Actual Cost of Work Performed，实际工作花的钱。也就是说，实际上我们花了多少时间。（实际完成12个任务，实际上花了127.5人日）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;我的理解就是，BCWS，大饼，意思是，我计划要干多少工作量。&lt;/p&gt;
&lt;p&gt;BCWP，实绩，我实际干完的活，按照原先的预算，值多少钱。&lt;/p&gt;
&lt;p&gt;ACWP，实际成本，我实际上花了多少钱。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由此衍生出来&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SPI&lt;/strong&gt;，Schedule Performance Index，计划表执行指标
$$
SPI=\frac{BCWP}{BCWS}
$$
如果SPI=1.0，刚好符合计划。SPI&amp;gt;1.0，卧槽超额完成！SPI&amp;lt;1.0，有点垃圾了慢吞吞的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SV&lt;/strong&gt;，Schedule Variance，进度表偏差
$$
SV=BCWP-BCWS
$$
同理，SV=0，刚好。SV&amp;gt;0，很牛逼了很快。SV&amp;lt;0，很慢了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPI&lt;/strong&gt;，Cost Performance Index，成本执行指标。
$$
CPI=\frac{BCWP}{ACWP}
$$
同理同理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CV&lt;/strong&gt;，Cost Variance，成本偏差
$$
CV=BCWP-ACWP
$$
同理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题1：&lt;/strong&gt; 假设你是一个软件项目管理者，受命为一个小型软件项目进行挣值统计。这个项目共计划了56个工作任务，估计需要582人日才能完成，但是，按照项目进度，现在应该完成15个任务，下面给出相关进度安排数据（单位：人日）&lt;/p&gt;
&lt;p&gt;请你做出挣值分析，计算该项目的进度表执行指标SPI、进度偏差SV、预定完成百分比、完成百分比、成本执行指标CPI和成本偏差CV。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;任务&lt;/th&gt;
&lt;th&gt;计划工作量&lt;/th&gt;
&lt;th&gt;实际工作量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;12.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;15.0&lt;/td&gt;
&lt;td&gt;11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;13.0&lt;/td&gt;
&lt;td&gt;17.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;18.0&lt;/td&gt;
&lt;td&gt;19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;14.0&lt;/td&gt;
&lt;td&gt;14.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;16.0&lt;/td&gt;
&lt;td&gt;－&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;－&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;－&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Budget Cost of Work Scheduled, BCWS
$$
BCWS=12.0+15.0+13.0+8.0+...+14.0+16.0+6.0+8.0=156.5
$$
Budget Cost of Work Performed, BCWP
$$
BCWP=12.0+15.0+13.0+8.0+...+14.0=126.5
$$
Acutal Cost of Work Performed, ACWP
$$
ACWP=12.5+11.0+17.0+...+4.0+14.5=127.5
$$&lt;/p&gt;
&lt;p&gt;所以SPI，Scheduled Performance Index
$$
SPI=\frac{126.5}{156.5}=0.8083
$$
进度偏差，Scheduled Variance
$$
SV=126.5-156.5=-30
$$
预定完成百分比
$$
\frac{156.5}{582}\times 100%=26.89%
$$
完成百分比
$$
\frac{126.5}{582}\times 100%=21.74%
$$
成本执行指标CPI，Cost Performance Index
$$
CPI=\frac{BCWP}{ACWP}=\frac{126.5}{127.5}=0.9922
$$
成本偏差CV, Cost Variance
$$
CV=126.5-127.5=-1
$$&lt;/p&gt;
&lt;h3&gt;缺陷放大和消除&lt;/h3&gt;
&lt;p&gt;缺陷放大模型&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230030102931.png&quot; alt=&quot;image-20251230030102931&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这里通过的错误，意思是传递下去了&lt;/p&gt;
&lt;p&gt;假设&lt;strong&gt;设计阶段&lt;/strong&gt;有10个错误传给&lt;strong&gt;编码阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;错误类型&lt;/th&gt;
&lt;th&gt;数量&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;通过的错误&lt;/td&gt;
&lt;td&gt;5个&lt;/td&gt;
&lt;td&gt;设计的错，编码时没发现，原样保留&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;放大的错误&lt;/td&gt;
&lt;td&gt;3个→9个&lt;/td&gt;
&lt;td&gt;设计的3个错，导致编码时产生9个bug（放大3倍）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新产生的错误&lt;/td&gt;
&lt;td&gt;6个&lt;/td&gt;
&lt;td&gt;编码自己犯的新错（比如语法错误）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传到下一步&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20个&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5+9+6 = 20个错误传给测试阶段&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230030412520.png&quot; alt=&quot;image-20251230030412520&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230030449580.png&quot; alt=&quot;image-20251230030449580&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;4  软件设计&lt;/h2&gt;
&lt;p&gt;这一部分重点是结构化程序设计。&lt;/p&gt;
&lt;p&gt;比如一个判断三角形类型的代码，可以写成这样：&lt;/p&gt;
&lt;p&gt;（写成这样也是神人了，总之意思就是写得乱七八糟的，&lt;code&gt;goto&lt;/code&gt;乱飞。）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229100531576.png&quot; alt=&quot;image-20251229100531576&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;然后如果改成结构化写法，就改成这样：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229100717460.png&quot; alt=&quot;image-20251229100717460&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;老师的PPT这样的：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229111335601.png&quot; alt=&quot;image-20251229111335601&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;我的理解如下。&lt;/p&gt;
&lt;p&gt;病形程序就是有死路的程序，烂代码。比如死循环什么的。反正就是史，有BUG，永远达不到结束。&lt;/p&gt;
&lt;p&gt;好形程序就是正常写，没有死路，但是可能有多出口，具体表现就是，可能有不止一个&lt;code&gt;return&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;适形程序，就是单出口，只有一个&lt;code&gt;return&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;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;
&lt;p&gt;（PPT上叽里咕噜说了那么多，我的理解是，简单来说，就是任何程序，都可以去掉&lt;code&gt;goto&lt;/code&gt;，改成全部使用&lt;code&gt;if&lt;/code&gt;和&lt;code&gt;while&lt;/code&gt;这样的。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数据流图、程序结构图&lt;/h3&gt;
&lt;p&gt;这里一个经典考题是数据流图转程序结构图。&lt;/p&gt;
&lt;p&gt;**题1：**请将以下图书销售系统数据流图转换为程序结构图。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230032607817.png&quot; alt=&quot;image-20251230032607817&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在数据流图中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;蓝色方框，是外部实体。系统外的人或组织。&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;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230041024897.png&quot; alt=&quot;image-20251230041024897&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;然后再看前面的图&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230041330363.png&quot; alt=&quot;image-20251230041330363&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;画出来&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230043847675.png&quot; alt=&quot;image-20251230043847675&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;判定表、判定树&lt;/h3&gt;
&lt;p&gt;判定表和判定树，都是用来专门处理有很多&lt;code&gt;if&lt;/code&gt;条件的情况。&lt;/p&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;残障乘客有优惠&lt;/li&gt;
&lt;li&gt;超重部分要额外收费&lt;/li&gt;
&lt;li&gt;不同情况收费标准不一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要直接写代码，会有大量的嵌套&lt;code&gt;if-else&lt;/code&gt;。这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (国内乘客) {
    if (残障) {
        if (超重) {
            // ...
        } else {
            // ...
        }
    } else {
        if (超重) {
            // ...
        }
    }
} else {
    // ...一堆嵌套
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后判定表、判定树，就是在写代码之前，先把所有情况理清楚的，理清有多少种条件、每种组合应该做什么。&lt;/p&gt;
&lt;p&gt;这样做完后，一目了然，写代码就不会漏情况。&lt;/p&gt;
&lt;h4&gt;判定表&lt;/h4&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251228214322568.png&quot; alt=&quot;image-20251228214322568&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;如上图，红色区域是条件列表，绿色区域是处理结果。&lt;/p&gt;
&lt;p&gt;怎么看这个表呢，右边两个区域，竖着看，比如：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251228214502195.png&quot; alt=&quot;image-20251228214502195&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这一列，意思是，如果满足&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;功率&amp;gt;50马力（Yes），是&lt;/li&gt;
&lt;li&gt;维修记录不全（Yes），是&lt;/li&gt;
&lt;li&gt;已运行&amp;gt;10年（Yes），是&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么，处理的结果是优先处理。&lt;/p&gt;
&lt;p&gt;同理，下一列就是，如果满足&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;功率&amp;gt;50马力（Yes），是&lt;/li&gt;
&lt;li&gt;维修记录不全（Yes），是&lt;/li&gt;
&lt;li&gt;已运行&amp;gt;10年（No），否&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么，处理的结果也是优先处理。&lt;/p&gt;
&lt;p&gt;**题1：**假设某航空公司规定，乘客可以免费托运重量不超过30kg的行李&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当行李重量超过30kg时，对头等舱的国内乘客超重部分每公斤收费4元，对其他舱的国内乘客超重部分每公斤收费6元&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;/p&gt;
&lt;ul&gt;
&lt;li&gt;行李重量 W 小于30kg&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;ul&gt;
&lt;li&gt;免费&lt;/li&gt;
&lt;li&gt;(W-30)*4&lt;/li&gt;
&lt;li&gt;(W-30)*6&lt;/li&gt;
&lt;li&gt;(W-30)*8&lt;/li&gt;
&lt;li&gt;(W-30)*12&lt;/li&gt;
&lt;li&gt;(W-30)*2&lt;/li&gt;
&lt;li&gt;(W-30)*3&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;行李重量 W 小于30kg&lt;/th&gt;
&lt;th&gt;Y&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;国内乘客（否则外国乘客）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;头等舱&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;残障乘客&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(W-30)*12&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;（这里第一列已经是优化后的结果了，对于 行李重量 W 小于30kg 为Y的，不需要重复后面的其他元素判定。&lt;/p&gt;
&lt;p&gt;**题2：**某银行发放贷款原则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于贷款未超过限额的客户，允许立即贷款。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于贷款超过限额的客户，若过去还款记录好且本次贷款在2万元以下，可作出贷款安排；否则拒绝贷款。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请绘出发放贷款的判定表并优化&lt;/p&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;p&gt;**题3：**某维修站对“功率大于50马力”的机器且“维修记录不全”或“已运行十年以上”的机器应给予优先维修，否则作一般处理。请绘制决策表。&lt;/p&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;h4&gt;判定树&lt;/h4&gt;
&lt;p&gt;判定表含义比较复杂，需要理解一下，含义不清晰，而且数据元素比较多的时候，有点复杂。&lt;/p&gt;
&lt;p&gt;所以有判定树，作为判定表的变种，更一目了然。&lt;/p&gt;
&lt;p&gt;前面的题1，就可以画成下面这样：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251228223102700.png&quot; alt=&quot;image-20251228223102700&quot; style=&quot;zoom: 20%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;优点是形式很简单，缺点是最后一个数据元素（比如残疾乘客/正常乘客）要写好多遍。&lt;/p&gt;
&lt;p&gt;**题1：**某图书发行公司采用下列政策优惠用户。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;书店订购20册以上优惠25%，否则不优惠;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;图书馆和个人订购6册以下则不优惠；6—19册则优惠5%，20—49册则优惠10%，50册以上则优惠15%。&lt;/p&gt;
&lt;p&gt;提示：有多少种备选方案，决策树就有多少个分枝。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251228231038575.png&quot; alt=&quot;image-20251228231038575&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;5  软件建模&lt;/h2&gt;
&lt;h3&gt;状态图&lt;/h3&gt;
&lt;p&gt;状态图是类似下面这样子的：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230125258497.png&quot; alt=&quot;image-20251230125258497&quot; style=&quot;zoom: 35%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这里其中的&lt;code&gt;do/action&lt;/code&gt;意思是，里面可以嵌套其他的状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;其中，&lt;strong&gt;Event(事件)&lt;/strong&gt; = &quot;发生了什么&quot; → &lt;strong&gt;触发器&lt;/strong&gt; &lt;strong&gt;Condition(条件)&lt;/strong&gt; = &quot;是否满足要求&quot; → &lt;strong&gt;检查器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想象你在家里:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event(事件)&lt;/strong&gt;: 门铃响了  ← 这是一个&quot;发生的动作&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Condition(条件)&lt;/strong&gt;: 检查猫眼看是不是快递员  ← 这是一个&quot;判断条件&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action(动作)&lt;/strong&gt;: 开门  ← 这是你要执行的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写出来就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;门铃响了 [是快递员] / 开门
Event   [Condition] / Action
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Condition&lt;/code&gt;是可以不需要的，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;按钮按下 / 启动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要任何条件，按下就启动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;举个例子1：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230125358387.png&quot; alt=&quot;image-20251230125358387&quot; style=&quot;zoom: 45%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;其中这种地方：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230125859408.png&quot; alt=&quot;image-20251230125859408&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;是可以嵌套的。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230125946683.png&quot; alt=&quot;image-20251230125946683&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看见，往里面嵌套了&lt;code&gt;Get Coffee&lt;/code&gt;的具体的东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题1：&lt;/strong&gt; 某信用卡系统账户具有使用状态和冻结状态，其中使用状态又包括正常状态和透支状态两种子状态。&lt;/p&gt;
&lt;p&gt;如果账户余额小于零则进入透支状态，透支状态时既可以存款又可以取款，但是透支金额不能超过5000元；&lt;/p&gt;
&lt;p&gt;如果余额大于零则进入正常状态，正常状态时既可以存款又可以取款；&lt;/p&gt;
&lt;p&gt;如果连续透支100天，则进入冻结状态，冻结状态下既不能存款也不能取款，必须要求银行工作人员解冻。&lt;/p&gt;
&lt;p&gt;用户可以在使用状态或冻结状态下请求注销账户。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230133022739.png&quot; alt=&quot;image-20251230133022739&quot; style=&quot;zoom: 43%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;6  软件测试&lt;/h2&gt;
&lt;h3&gt;MTTF计算题&lt;/h3&gt;
&lt;p&gt;MTTF，Mean Time To Failure，是平均无故障时间。&lt;/p&gt;
&lt;p&gt;也就是说，程序平均能运行多久才会出错。&lt;/p&gt;
&lt;p&gt;做题是基于一个叫“Shooman模型”的公式：
$$
MTTF=\frac{程序总指令数}{K\times (程序中总错误数-已修复的错误数)}
$$
可以写成：
$$
MTTF=\frac{I_t}{K\times(E_t-E_c)}
$$
上面，程序总错误数 $E_t$，一般来说是题目要我们求的（也就是程序总共估计有多少个Bug，在测试过程中没测试完前，只能估计出来）&lt;/p&gt;
&lt;p&gt;然后 $K$ 是一个常数，一般也是未知数，是一个固定的系数，不需要理解含义，算出来就行。&lt;/p&gt;
&lt;p&gt;然后MTTF与测试时间，在做题时假设是线性关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题1&lt;/strong&gt;：对一个长度为100000条指令的程序进行集成测试期间记录如下面的数据：&lt;/p&gt;
&lt;p&gt;(A) 7月1日集成测试开始没有发现错误&lt;/p&gt;
&lt;p&gt;(B) 8月2日总共改正了100个错误,此时MTTF=0.4H&lt;/p&gt;
&lt;p&gt;(C) 9月1日:总共改正300个错误,此时MTTF=2H.&lt;/p&gt;
&lt;p&gt;根据上述数据完成下列各题:&lt;/p&gt;
&lt;p&gt;(1) 估计程序中的错误总数&lt;/p&gt;
&lt;p&gt;(2) 为使MTTF达到10H必须测试和调试这个程序多长时间?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（1）列方程
$$
0.4=\frac{10^5}{K\times(E_t-100)}\
2=\frac{10^5}{K\times(E_t-300)}
$$
解得：
$$
E_t=350\
K=1000
$$
也就是总错误数 $E_t=350$&lt;/p&gt;
&lt;p&gt;（2）&lt;/p&gt;
&lt;p&gt;如果 $MTTF=10H$
$$
10=\frac{10^5}{1000\times(350-E_c)}\
$$
得到
$$
E_c=340
$$
截止到9月1日，总共已经改正300个错误，还剩40个。&lt;/p&gt;
&lt;p&gt;**题2： **对一个包含10000条机器指令的程序进行一个月集成测试后,总共改正了15个错误,此时MTTF= 10h；经过两个月测试后,总共改正了25个错误(第二个月改正了10个错误) ,MTTF= 15h。&lt;/p&gt;
&lt;p&gt;要求:&lt;/p&gt;
&lt;p&gt;(1) 根据上述数据确定MTTF与测试时间之间的函数关系,画出MTTF与测试时间τ的关系曲线。在画这条曲线时做了什么假设？&lt;/p&gt;
&lt;p&gt;(2) 为做到MTTF= 100h,必须进行多长时间的集成测试?当集成测试结束时总共改正了多少个错误,还有多少个错误潜伏在程序中？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（1）假设MTTF与测试时间是线性关系。
$$
MTTF=at+b
$$
一个月集成测试后，MTTF=10h，也就是说，$t_1=720h$，$MTTF=10h$&lt;/p&gt;
&lt;p&gt;两个月后，同理，$t_2=1440h$，$MTTF=15h$
$$
10=a\times720+b\
15=a\times 1440+b
$$
解得
$$
a=\frac 5 {720}=\frac{1}{144}\
b=5
$$
曲线：
$$
MTTF=\frac{1}{144}\times t+5
$$
（2）
$$
100=\frac{1}{144}t+5\
t=95\times 144=13680\ h=570\ day=19\ month
$$
也就是19个月。&lt;/p&gt;
&lt;p&gt;然后要算还剩多少个错误，需要来算 $E_t, K$
$$
10=\frac{10^4}{K\times(E_t-15)}\
15=\frac{10^4}{K\times(E_t-25)}
$$
解得到
$$
E_t=45\
K=\frac{10^3}{30}=\frac{100}{3}
$$
当 $MTTF=100$，剩余 3 个错误。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229191822000.png&quot; alt=&quot;image-20251229191822000&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251229191836969.png&quot; alt=&quot;image-20251229191836969&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>厦门大学计算机《软件工程》专业课速通笔记（2）</title><link>https://fuwari.vercel.app/posts/course/software-engineering/se-notes-2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/software-engineering/se-notes-2/</guid><pubDate>Tue, 30 Dec 2025 04:46:36 GMT</pubDate><content:encoded>&lt;p&gt;前面是老师画的重点。但是显然这些会了也不一定够。&lt;/p&gt;
&lt;p&gt;期末题型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择题10道，3分，共30分。&lt;/li&gt;
&lt;li&gt;简答题5道，5分，共25分。&lt;/li&gt;
&lt;li&gt;计算题2道，10分，共20分。&lt;/li&gt;
&lt;li&gt;画图题3道，10、10、15分，共35分（这个我感觉就是实验的图啊，占比还挺大的）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（我也不知道为什么加起来是110分啊）&lt;/p&gt;
&lt;p&gt;&lt;s&gt;复习&lt;/s&gt; 速通 的话，可以先把实验看一下。&lt;/p&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;实验1：用例图、需求文档&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为了提升图书馆的服务水平和运营效率，xx大学拟引进全新的图书管理系统。该系统不仅包含基础的管理功能，还将集成先进的统计功能。&lt;/p&gt;
&lt;p&gt;系统将提供一个全面的图书检索平台，&lt;strong&gt;用户可以通过多种关键字如书名、作者、ISBN号等进行图书查询&lt;/strong&gt;，确保快速准确地定位到所需资源。在线借阅服务将允许用户便捷地完成借阅手续，并且能够在借阅期限即将到期时通过系统进行续借，避免逾期罚款。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图书预约&lt;/strong&gt;功能将进一步提高图书的利用率和用户的满意度。当热门图书暂不可用时，用户可以预约，并在图书归还时通过系统通知及时得知。&lt;/p&gt;
&lt;p&gt;在用户自我管理方面，&lt;strong&gt;系统允许用户查看和管理自己的借阅记录&lt;/strong&gt;，包括&lt;strong&gt;借阅历史、当前借阅状态和预约信息&lt;/strong&gt;，以便于用户更好地跟踪自己的借阅活动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图书管理方面，管理员能够通过系统轻松进行图书的添加、编辑和删除&lt;/strong&gt;，同时，&lt;strong&gt;系统将提供强大的统计功能，包括图书借阅次数、用户借阅频率、热门书籍排行榜&lt;/strong&gt;等。这些统计数据将帮助管理员分析图书的使用情况，为图书采购、库存管理和淘汰提供数据支持。&lt;/p&gt;
&lt;p&gt;系统还将支持&lt;strong&gt;图书推荐&lt;/strong&gt;功能，&lt;strong&gt;教师或其他指定用户&lt;/strong&gt;可以根据教学内容或特定主题向所有用户推荐图书，推荐列表将在系统中特别展示，以促进图书的发现和借阅。&lt;/p&gt;
&lt;p&gt;对于系统安全管理，&lt;strong&gt;超级管理员将拥有最高权限，能够管理其他管理员账户，包括创建新账户、调整权限和删除账户&lt;/strong&gt;。此外，系统将&lt;strong&gt;自动记录所有操作日志&lt;/strong&gt;，超级管理员可以通过统计功能来审查这些日志，以监控系统的使用情况和安全性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;用例图&lt;/h3&gt;
&lt;p&gt;先明确有几个人：用户、管理员、超级管理员、教师或其他指定用户&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230060326674.png&quot; alt=&quot;image-20251230060326674&quot; style=&quot;zoom: 43%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;需求文档&lt;/h3&gt;
&lt;p&gt;没讲评，估计不会考。&lt;/p&gt;
&lt;h2&gt;实验2：数据流图、ER图&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;在现代健身行业中，一个高效的管理系统对于提升服务质量和会员体验至关重要。设想我们正在设计一个全面的健身房会员管理系统，该系统旨在优化会员服务流程、课程资源分配和设备管理。&lt;/p&gt;
&lt;p&gt;系统首先需要管理&lt;strong&gt;会员&lt;/strong&gt;的详细信息，包括会员的&amp;lt;u&amp;gt;姓名、性别、年龄、联系方式和会员等级&amp;lt;/u&amp;gt;。每位会员都有一个唯一的&amp;lt;u&amp;gt;会员ID&amp;lt;/u&amp;gt;，以便系统进行识别和管理。会员可以通过系统进行注册，成为新会员或续费现有会员资格。此外，系统还应记录会员的&amp;lt;u&amp;gt;健身目标和偏好&amp;lt;/u&amp;gt;，以便提供个性化的服务。&lt;/p&gt;
&lt;p&gt;其次，系统需要提供丰富的&lt;strong&gt;课程&lt;/strong&gt;信息，涵盖&amp;lt;u&amp;gt;课程名称、教练姓名、上课时间、地点和课程容量&amp;lt;/u&amp;gt;。课程可以分为不同的&amp;lt;u&amp;gt;类型&amp;lt;/u&amp;gt;，如瑜伽、力量训练、有氧运动等。&lt;strong&gt;教练&lt;/strong&gt;作为课程的一部分，其信息也需要被记录，包括教练的&amp;lt;u&amp;gt;姓名、专长和联系方式&amp;lt;/u&amp;gt;。系统应支持&lt;strong&gt;管理员&lt;/strong&gt;添加新课程、修改课程信息以及删除不再开设的课程。&lt;/p&gt;
&lt;p&gt;此外，系统还需管理健身房的各类设备，如跑步机、哑铃、拉伸器械等。每台设备都有唯一的设备ID、名称、类型和使用状态。设备的状态可以是“可用”、“维修中”或“已报废”。系统应能够记录设备的维护历史，以确保设备的良好运行，并提供设备使用率的统计报表。&lt;/p&gt;
&lt;p&gt;会员可以通过系统预约感兴趣的课程，预约记录应包含会员ID、课程ID、预约时间和预约状态（如“已预约”、“已取消”）。系统应支持会员查看自己的预约历史，并允许他们在需要时取消预约。预约功能还应考虑课程的容量限制，避免超员预约。&lt;/p&gt;
&lt;p&gt;作为整个系统的中枢，健身房管理系统负责协调和管理会员、课程、设备和预约等各项功能。系统应能够生成各类报表，如会员活跃度报表、课程参与度报表和设备使用率报表，以便管理层进行决策。系统还应具备一定的灵活性，支持管理员进行各类管理和配置操作。&lt;/p&gt;
&lt;p&gt;假如你是本项目的项目经理，请完成以下任务：&lt;/p&gt;
&lt;p&gt;（1） 绘制该健身房管理系统的数据流图&lt;/p&gt;
&lt;p&gt;（2） 绘制该健身房管理系统的ER图&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数据流图&lt;/h3&gt;
&lt;p&gt;首先先看一下谁在用这个系统：会员、管理员。（教练不使用这个系统，所以不是实体）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230061155409.png&quot; alt=&quot;image-20251230061155409&quot; style=&quot;zoom: 43%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;ER图&lt;/h3&gt;
&lt;p&gt;（这个数据库学过）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230062017891.png&quot; alt=&quot;image-20251230062017891&quot; style=&quot;zoom: 43%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;实验3：数据流图转程序结构图、盒图&lt;/h2&gt;
&lt;h3&gt;数据流图转程序结构图&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;现有一个图书管理系统，其中修改图书信息的数据流图如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230062156055.png&quot; alt=&quot;image-20251230062156055&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;请将该数据流图映射为体系结构图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和前面的笔记1的题目一样，拆成三份，输入、处理、输出。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230063001013.png&quot; alt=&quot;image-20251230063001013&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;然后中间处理部分也是类似的：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230063558680.png&quot; alt=&quot;image-20251230063558680&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230065633241.png&quot; alt=&quot;image-20251230065633241&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;盒图&lt;/h3&gt;
&lt;p&gt;没讲评，我猜不会考。&lt;/p&gt;
&lt;h2&gt;实验4：状态图和顺序图&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下是一个智能灌溉系统需求描述：&lt;/p&gt;
&lt;p&gt;如果&lt;strong&gt;主开关置于“灌溉”状态&lt;/strong&gt;，智能灌溉系统的控制器负责开关水泵，监视灌溉系统的水流量和土壤湿度，进而调节灌溉量。当土壤湿度降至（Hr-10）%以下，控制器启动水泵。这里Hr 是用户设定的理想土壤湿度。水泵启动过程如下：&lt;/p&gt;
&lt;p&gt;（1）控制器向水泵的马达发送信号；&lt;/p&gt;
&lt;p&gt;（2）控制器监视马达速度。马达达到正常操作速度时，启动电磁阀并打开水阀；&lt;/p&gt;
&lt;p&gt;（3）控制器监视土壤湿度，一旦土壤湿度达到预定值，发信号关闭水阀，水停止灌溉；&lt;/p&gt;
&lt;p&gt;（4）如果发生异常情况，水流量传感器和土壤湿度传感器向控制器发信号。此时控制器发信号关闭系统。&lt;/p&gt;
&lt;p&gt;（5）一旦土壤湿度达到（Hr-10）%，控制器首先关闭水阀，延迟5秒钟后关闭水泵马达。&lt;/p&gt;
&lt;p&gt;系统须满足以下限制：&lt;/p&gt;
&lt;p&gt;（1）水泵停机后重启必须延迟5秒钟；&lt;/p&gt;
&lt;p&gt;（2）在主开关关闭或水阀关闭5秒钟内应指示水泵停机。&lt;/p&gt;
&lt;p&gt;试据此对系统进行设计，绘制控制器的状态图，同时用顺序图描述锅炉启动过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;状态图&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230145421438.png&quot; alt=&quot;image-20251230145421438&quot; style=&quot;zoom: 43%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;实验6：等价类划分&lt;/h2&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230152930994.png&quot; alt=&quot;image-20251230152930994&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230152951137.png&quot; alt=&quot;image-20251230152951137&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230153001542.png&quot; alt=&quot;image-20251230153001542&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230153016659.png&quot; alt=&quot;image-20251230153016659&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20251230153027683.png&quot; alt=&quot;image-20251230153027683&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>蓝桥杯python组考前速通笔记</title><link>https://fuwari.vercel.app/posts/acm/lanqiao-python-before-exam/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/lanqiao-python-before-exam/</guid><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;找到当年蓝桥杯python组考前速通笔记，我当时速通完爽拿了个Python A组国一，应该还是有一点参考价值的！&lt;/p&gt;
&lt;p&gt;有一些东西差了点，时间久远忘了当时还差什么了。&lt;/p&gt;
&lt;p&gt;我考前准备的速通todo：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] 排序&lt;/li&gt;
&lt;li&gt;[x] 栈，队列&lt;/li&gt;
&lt;li&gt;[x] 优先队列&lt;/li&gt;
&lt;li&gt;[x] 数学库&lt;/li&gt;
&lt;li&gt;[x] 对拍&lt;/li&gt;
&lt;li&gt;[x] 扩大递归深度&lt;/li&gt;
&lt;li&gt;[x] 时间库&lt;/li&gt;
&lt;li&gt;[x] ASCII码&lt;/li&gt;
&lt;li&gt;[x] 输出格式&lt;/li&gt;
&lt;li&gt;[ ] 哈希（忘了为什么当时没勾这个）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;蓝桥杯python组感觉主要是有一些和C++不太一样的东西，如果比较少用python写题就容易考场上坐牢。&lt;/p&gt;
&lt;p&gt;下面是我考前准备的一些主要的不一样的要点。&lt;/p&gt;
&lt;h2&gt;零. IDLE配置&lt;/h2&gt;
&lt;p&gt;蓝桥杯python指定编译器用python IDLE，这个默认配置不太好用。高亮什么的很奇怪，要自己设置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250614230110719.png&quot; alt=&quot;image-20250614230110719&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Options-Configure IDLE&lt;/code&gt;设置代码高亮。&lt;/p&gt;
&lt;p&gt;字体我设置的是&lt;code&gt;Cascadia Mono&lt;/code&gt;，也可以用几乎每台电脑都有的&lt;code&gt;Consolas&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;主题在这里改成黑色，或者就白色的也行。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250614230403100.png&quot; alt=&quot;image-20250614230403100&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;代码补全，有些低版本的IDLE没带，所以我平时准备蓝桥杯时也不开代码补全。就不记录了。推荐准备的时候用考场上一样的配置（也就是先不用vscode，尽可能多用IDLE）。&lt;/p&gt;
&lt;h2&gt;一. 调试工具&lt;/h2&gt;
&lt;h3&gt;输入重定向&lt;/h3&gt;
&lt;p&gt;对应C++里的&lt;code&gt;freopen&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sys.stdin = open(&quot;1.txt&quot;, &quot;r&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对拍&lt;/h3&gt;
&lt;p&gt;一个简单的对拍代码，调用暴力的&lt;code&gt;std.py&lt;/code&gt;、测试的未必正确的正解&lt;code&gt;test.py&lt;/code&gt;、数据生成器&lt;code&gt;gen.py&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import subprocess as sp
import sys

n = 100
for i in range(n):
    print(f&quot;test #{i} &quot;, end=&quot;&quot;)
    sp.run([&quot;python&quot;, &quot;gen.py&quot;], stdout=open(&quot;in.txt&quot;, &quot;w&quot;))
    sp.run([&quot;python&quot;, &quot;std.py&quot;], stdin=open(&quot;in.txt&quot;,&quot;r&quot;), stdout=open(&quot;out1.txt&quot;, &quot;w&quot;))
    sp.run([&quot;python&quot;, &quot;test.py&quot;], stdin=open(&quot;in.txt&quot;,&quot;r&quot;), stdout=open(&quot;out2.txt&quot;, &quot;w&quot;))
    with open(&quot;out1.txt&quot;, &quot;r&quot;) as f1, open(&quot;out2.txt&quot;, &quot;r&quot;) as f2:
        out1 = f1.read()
        out2 = f2.read()
        if out1 == out2:
            print(&quot;pass&quot;)
        else:
            print(&quot;failed&quot;)
            break
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 基础问题&lt;/h2&gt;
&lt;h3&gt;排序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;a = sorted(a, key = lambda x: x[1], reverse = True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日期与时间&lt;/h3&gt;
&lt;p&gt;直接看文档&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import datetime
help(datetime)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在打印出来的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CLASSES
    builtins.object
        date
            datetime
        time
        timedelta
        tzinfo
            timezone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见继承关系。例如&lt;code&gt;datetime&lt;/code&gt;继承自&lt;code&gt;date&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以这题为例：https://www.lanqiao.cn/problems/16955/learning/&lt;/p&gt;
&lt;p&gt;一些函数直接看help。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输出一个整数，表示在公元 $1901$ 年 $1$ 月 $1$ 日到 $2000$ 年 $12$ 月 $31$ 日期间，有多少个月的第一天是周日。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;from datetime import date, timedelta

s = date(1901, 1, 1)
t = date(2000, 12, 31)

dt = timedelta(days = 1)
last = -1
ans = 0
while s != t:
    if last != s.month:
        if s.weekday() == 6:
            ans += 1
    last = s.month
    s += dt
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有这题：https://www.lanqiao.cn/problems/2271/learning/&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from datetime import date, timedelta

now = date(2022, 1, 1)
ans = 0

while now.year == 2022:
    now += timedelta(days=1)
    if now.weekday() == 5 or now.weekday() == 6 or now.day in [1, 11, 21, 31]:
        ans += 1
    
print(ans)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及这题https://www.lanqiao.cn/problems/1452/learning/&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os, time

t = int(input()) // 1000

print(time.strftime(&quot;%H:%M:%S&quot;, time.gmtime(t)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;类似c++。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 百分比格式
ratio = 0.75
print(f&quot;{ratio:.1%}&quot;)           # 输出: 75.0%

# 科学计数法
large_num = 1234567
print(f&quot;{large_num:.2e}&quot;)       # 输出: 1.23e+06

# 二、八、十六进制
number = 42
print(f&quot;{number:b}&quot;)            # 输出: 101010 (二进制)
print(f&quot;{number:o}&quot;)            # 输出: 52 (八进制)
print(f&quot;{number:x}&quot;)            # 输出: 2a (十六进制小写)
print(f&quot;{number:X}&quot;)            # 输出: 2A (十六进制大写)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保留小数例题：https://www.lanqiao.cn/problems/8631/learning/&lt;/p&gt;
&lt;h3&gt;进制转换&lt;/h3&gt;
&lt;p&gt;实际上python的&lt;code&gt;int&lt;/code&gt;函数是可以用&lt;code&gt;base&lt;/code&gt;来指定转换基数的。&lt;/p&gt;
&lt;p&gt;例题：https://www.lanqiao.cn/problems/19807/learning&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys

print(int(&quot;10&quot;, base=2))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三. 注意事项&lt;/h2&gt;
&lt;h3&gt;递归深度&lt;/h3&gt;
&lt;p&gt;python默认递归深度大概1000，容易爆栈。&lt;/p&gt;
&lt;p&gt;查看递归深度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
print(sys.getrecursionlimit())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置递归深度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
print(sys.setrecursionlimit(3000))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以&lt;code&gt;help(sys)&lt;/code&gt;来查看，感觉这个不好记住。&lt;/p&gt;
&lt;h3&gt;ASCII码转换&lt;/h3&gt;
&lt;p&gt;获取一个字符的ASCII码，可以用&lt;code&gt;ord&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch = &apos;a&apos;
print(ord(ch))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要将一个数值，按照ASCII码，转换成字符，就要用到&lt;code&gt;chr&lt;/code&gt;函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch2 = chr(97)
print(ch2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例题链接：https://www.lanqiao.cn/problems/1446/learning&lt;/p&gt;
&lt;h2&gt;四. 数据结构&lt;/h2&gt;
&lt;h3&gt;优先队列：&lt;code&gt;heapq&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;python中用&lt;code&gt;heapq&lt;/code&gt;库来实现优先队列功能。&lt;/p&gt;
&lt;p&gt;例题：https://www.lanqiao.cn/problems/741/learning&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys, os

# sys.stdin = open(&quot;1.txt&quot;, &quot;r&quot;)

n = int(input())
a = list(map(int, input().split()))
ans = 0

import heapq
que = []
for x in a:
    heapq.heappush(que, x)
while len(que) != 1:
    u = heapq.heappop(que)
    v = heapq.heappop(que)
    ans += u + v
    heapq.heappush(que, u + v)
print(ans)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双端队列：&lt;code&gt;deque&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;用的是&lt;code&gt;collections&lt;/code&gt;库的&lt;code&gt;deque&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例题：https://www.lanqiao.cn/problems/511/learning&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys, os

m, n = map(int, input().split())
words = list(map(int, input().split()))

from collections import deque
que = deque()
inque = set()
ans = 0

for w in words:
    if w in inque:
        continue
    ans += 1
    if len(que) &amp;gt;= m:
        old = que.popleft()
        inque.remove(old)
    que.append(w)
    inque.add(w)
print(ans)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五. 数学库&lt;/h2&gt;
&lt;p&gt;python数学库比C++感觉多了不少东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合数函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;factorial(x)&lt;/code&gt; - 计算阶乘，直接返回x!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;comb(n, k)&lt;/code&gt; - 计算组合数$C(n,k)$，即二项式系数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;perm(n, k)&lt;/code&gt; - 计算排列数$P(n,k)$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数论函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gcd(*integers)&lt;/code&gt; - 计算多个整数的最大公约数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lcm(*integers)&lt;/code&gt; - 计算多个整数的最小公倍数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isqrt(n)&lt;/code&gt; - 计算整数平方根（向下取整）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;高精度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fsum(seq)&lt;/code&gt; - 高精度浮点数求和，避免累积误差&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ceil(x)&lt;/code&gt;和&lt;code&gt;floor(x)&lt;/code&gt; - 向上和向下取整&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)&lt;/code&gt; - 判断两个浮点数是否近似相等（处理浮点误差）&lt;/p&gt;
&lt;p&gt;除此之外，python内置了快速幂，&lt;code&gt;pow(base, exp[, mod])&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 带模的快速幂（更高效）
result = pow(2, 10, 1000000007)  # 计算2^10 % 1000000007
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>面向对象考前速通笔记 (1) 从过程抽象到C++</title><link>https://fuwari.vercel.app/posts/course/ooap/ooap-note-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/ooap/ooap-note-1/</guid><pubDate>Mon, 16 Jun 2025 09:13:07 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;现在是6月16号，差不多24个小时后进行面向对象考试，这是我的考前速通笔记。&lt;/p&gt;
&lt;p&gt;差不多零基础吧一学期一点没学（）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（考完补充：这门课厦门大学还是考得很easy的，速通24小时拿下95，耶。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;课本1~3章都是弱智语法直接skip了。&lt;/p&gt;
&lt;p&gt;第4章末尾开始讲一点C++。&lt;/p&gt;
&lt;p&gt;实际上可以直接从第5章开始学。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一. C/C++语法回顾&lt;/h2&gt;
&lt;h3&gt;数据类型&lt;/h3&gt;
&lt;p&gt;基本数据类型包括：int、char、float、double等&lt;/p&gt;
&lt;p&gt;构造数据类型包括：数组、指针、结构体、类等&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250617051633651.png&quot; alt=&quot;image-20250617051633651&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;1. 形参实参&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 函数定义 - 制定快递流程
void deliverPackage(string address, string recipient) {  // address和recipient是形参
    cout &amp;lt;&amp;lt; &quot;送货到：&quot; &amp;lt;&amp;lt; address &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;收件人：&quot; &amp;lt;&amp;lt; recipient &amp;lt;&amp;lt; endl;
}

// 函数调用 - 实际执行快递任务
int main() {
    deliverPackage(&quot;北京市朝阳区&quot;, &quot;张三&quot;);  // &quot;北京市朝阳区&quot;和&quot;张三&quot;是实参
    deliverPackage(&quot;上海市浦东区&quot;, &quot;李四&quot;);  // &quot;上海市浦东区&quot;和&quot;李四&quot;是实参
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 引用&lt;/h3&gt;
&lt;p&gt;引用是C++在C语言基础上新增的特性。C语言只有&lt;strong&gt;指针&lt;/strong&gt;来实现间接访问。&lt;/p&gt;
&lt;p&gt;什么是引用？&lt;strong&gt;引用&lt;/strong&gt;就像给一个人起&lt;strong&gt;别名&lt;/strong&gt;。你的真名是&quot;张三&quot;，但朋友们可以叫你&quot;小张&quot;，这两个名字指的是同一个人。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int original = 100;        // 原始变量
int&amp;amp; nickname = original;  // nickname 是 original 的引用（别名）

// 现在 original 和 nickname 指向同一块内存
cout &amp;lt;&amp;lt; original &amp;lt;&amp;lt; endl;  // 输出：100
cout &amp;lt;&amp;lt; nickname &amp;lt;&amp;lt; endl;  // 输出：100

nickname = 200;           // 通过别名修改值
cout &amp;lt;&amp;lt; original &amp;lt;&amp;lt; endl; // 输出：200 （原始变量也被改变了！）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;数据类型&amp;amp; 引用名 = 已存在的变量;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. C++函数的进一步讨论&lt;/h2&gt;
&lt;h3&gt;1. 带参数宏&lt;/h3&gt;
&lt;h3&gt;2. 内联函数&lt;/h3&gt;
&lt;p&gt;在程序中，一个常规的函数调用如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保存现场&lt;/strong&gt;：程序需要把当前的指令地址、用到的变量等信息压入一个叫做“栈 (Stack)”的内存区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转&lt;/strong&gt;：CPU 跳转到函数代码所在的内存地址。&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;li&gt;&lt;strong&gt;恢复现场&lt;/strong&gt;：从栈中弹出之前保存的信息，回到调用前的位置继续执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于大部分函数来说，这个跳转的开销微不足道。但如果一个函数&lt;strong&gt;非常简单&lt;/strong&gt;（比如就一行 &lt;code&gt;return a + b;&lt;/code&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;pre&gt;&lt;code&gt;// 请求将 add 函数内联
inline int add(int a, int b) {
    return a + b;
}

int main() {
    int sum = add(5, 3); // 调用内联函数
    return 0;
}


// 编译器看到了 inline 请求，并且觉得 add 函数足够简单
// 于是它把函数调用直接替换掉了

int main() {
    // int sum = add(5, 3); 被替换成了下面的代码
    int sum = 5 + 3; 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很容易想到，那递归函数也能直接替换吗？很明显不能。它都不知道自己要做几次，怎么替换呢？真替换了，编译阶段就会发生无限循环。所以编译器会直接放弃内联。&lt;/p&gt;
&lt;p&gt;给函数定义加上关键词 &lt;code&gt;inline&lt;/code&gt; 只是建议编译程序把该函数作为内联函数来处理，至于编译程序是否把它作为内联函数来实现，要视函数体的具体实现而定。比如递归函数就是加了也没用。&lt;/p&gt;
&lt;p&gt;除此之外，因为编译器是分块编译再链接的，一个&lt;code&gt; cpp&lt;/code&gt;文件生成一个&lt;code&gt;obj&lt;/code&gt;文件，最后用链接器链接起来，才生成&lt;code&gt;exe&lt;/code&gt;。如果用&lt;code&gt;inline&lt;/code&gt;，因为要内联时在编译时直接替换，所以是没有生成独立的函数地址的。因此一个内联函数不能在其他文件中被调用，除非通过&lt;code&gt;#include&lt;/code&gt; 放进来。&lt;/p&gt;
&lt;h3&gt;3. 函数参数默认值&lt;/h3&gt;
&lt;p&gt;C++允许给函数参数加默认值，这样调用的时候，如果这些有默认值的参数没指定值，就直接用默认参数。&lt;/p&gt;
&lt;p&gt;除了带默认值的参数必须放在形参表的最右侧外，还有一些注意点。&lt;/p&gt;
&lt;p&gt;首先，函数的默认参数是给 &lt;strong&gt;调用者 (caller)&lt;/strong&gt; 看的，而不是给函数定义看的。是&lt;strong&gt;编译器&lt;/strong&gt;在编译&lt;strong&gt;调用函数的代码&lt;/strong&gt;时需要。当编译器看到一个函数调用，比如 &lt;code&gt;my_print();&lt;/code&gt;，但这个函数实际上是 &lt;code&gt;void my_print(int level);&lt;/code&gt;，编译器需要一个默认值来填补那个被省略的参数。&lt;strong&gt;函数的定义（函数体 &lt;code&gt;{...}&lt;/code&gt;）是函数的具体实现，是不关心，也看不见这个默认值的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// --- logger.h ---
// 函数声明：在这里为 level 参数指定默认值 0
void log(const char* message, int level = 0);


// --- logger.cpp --

// 函数定义：这里不需要也不能重复指定默认值
// 它只负责实现功能
void log(const char* message, int level) {
    std::cout &amp;lt;&amp;lt; &quot;[Level &quot; &amp;lt;&amp;lt; level &amp;lt;&amp;lt; &quot;]: &quot; &amp;lt;&amp;lt; message &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&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;这个规则揭示了一个 C++ 的“陷阱”。因为编译器是独立编译每个 &lt;code&gt;.cpp&lt;/code&gt; 文件（翻译单元）的，所以理论上你可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// --- uploader.cpp ---
#include &amp;lt;iostream&amp;gt;
void upload(const char* filename, int timeout = 10); // 声明1：超时默认10秒

void perform_upload() {
    upload(&quot;data.zip&quot;); // 编译器看到默认值10，实际调用 upload(&quot;data.zip&quot;, 10)
}

// --- downloader.cpp ---
#include &amp;lt;iostream&amp;gt;
void upload(const char* filename, int timeout = 60); // 声明2：超时默认60秒

void some_other_task() {
    upload(&quot;report.pdf&quot;); // 编译器看到默认值60，实际调用 upload(&quot;report.pdf&quot;, 60)
}

// --- network.cpp --- (真正的函数定义)
#include &amp;lt;iostream&amp;gt;
void upload(const char* filename, int timeout) {
    std::cout &amp;lt;&amp;lt; &quot;Uploading &quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; &quot; with timeout &quot; &amp;lt;&amp;lt; timeout &amp;lt;&amp;lt; &quot;s.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个语法上允许，但是最好别干。&lt;/p&gt;
&lt;h3&gt;4. 函数名重载&lt;/h3&gt;
&lt;p&gt;这个是C++的特性，C语言没有这个东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 C 语言中&lt;/strong&gt;： 编译器在处理函数时，提供给链接器的“符号名”基本上就是函数名本身。比如你定义了一个函数 &lt;code&gt;void print_int(int x)&lt;/code&gt;，它在链接器看来可能就是 &lt;code&gt;_print_int&lt;/code&gt;。如果你再定义一个同名函数，链接器就会看到两个一模一样的名字，它会立刻报错：“错误：符号 &lt;code&gt;_print_int&lt;/code&gt; 被重复定义了！” 它无法区分这两个函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 C++ 语言中&lt;/strong&gt;： C++ 的编译器非常聪明。它在生成符号名的时候，会把&lt;strong&gt;函数的参数类型信息也编码进去&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void print(int i)&lt;/code&gt; 可能被编译器处理成 &lt;code&gt;_print_i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void print(double d)&lt;/code&gt; 可能被处理成 &lt;code&gt;_print_d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void print(char c)&lt;/code&gt; 可能被处理成 &lt;code&gt;_print_c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于链接器来说，&lt;code&gt;_print_i&lt;/code&gt;、&lt;code&gt;_print_d&lt;/code&gt; 和 &lt;code&gt;_print_c&lt;/code&gt; 是三个&lt;strong&gt;完全不同&lt;/strong&gt;的名字！因此它当然不会报错。正是这个底层的“名字修饰”机制，才使得函数重载在 C++ 中成为可能。&lt;/p&gt;
&lt;p&gt;这就是函数名重载，也是一种实现多态性的语言机制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;到这里可以理解多态了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多态性 (Polymorphism)&lt;/strong&gt;，从字面上看就是“多种形态”。在程序设计中，它的核心思想非常简洁：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;允许你使用一个统一的、通用的接口来处理不同类型的数据或对象，而这些数据或对象会根据自身的特性，以不同的方式响应这个接口。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;如果两个函数的参数类型和个数相同，只是返回值类型不同，不能重载函数名。&lt;/p&gt;
&lt;p&gt;为什么呢？因为仅仅从函数调用本身，编译器有时候根本无法判断程序员到底想用哪个版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;确定一个对重载函数的调用对应着哪一个重载函数定义的过程称为&lt;strong&gt;绑定（binding）&lt;/strong&gt;，又称定联、联编、捆绑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;绑定有静态绑定和动态绑定两种。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态绑定 (Static Binding)&lt;/strong&gt;：在&lt;strong&gt;编译时&lt;/strong&gt;就确定调用哪个函数。重载函数的绑定是静态绑定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态绑定 (Dynamic Binding)&lt;/strong&gt;：在&lt;strong&gt;运行时&lt;/strong&gt;才确定调用哪个函数。（这个你会在后面学习“多态”和“虚函数”时遇到，现在先不用管它）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与形参的匹配规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;精确匹配。要类型完全一致，或者经过一些细微转换，比如数组名到第一个元素指针，函数名到指针。&lt;/li&gt;
&lt;li&gt;提升匹配。&lt;code&gt;int&lt;/code&gt;提到&lt;code&gt;long&lt;/code&gt;之类的，&lt;code&gt;float&lt;/code&gt;变成&lt;code&gt;double&lt;/code&gt;或者&lt;code&gt;long double&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;标准匹配：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;任意算术类型之间可以互换&lt;/strong&gt;：这是最常见的。比如 &lt;code&gt;int&lt;/code&gt; 可以换成 &lt;code&gt;double&lt;/code&gt;，&lt;code&gt;double&lt;/code&gt; 可以换成 &lt;code&gt;int&lt;/code&gt;，&lt;code&gt;int&lt;/code&gt; 可以换成 &lt;code&gt;char&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enum&lt;/code&gt; (枚举) 可以换成 &lt;code&gt;int&lt;/code&gt; 等算术类型。&lt;/li&gt;
&lt;li&gt;数字 &lt;code&gt;0&lt;/code&gt; 可以换成任意指针类型（变成空指针）。 ...等等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有“标准转换”的地位是平等的，没有优先级。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为没有优先级，所以有多个转换选项的时候，就会报错。&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void print(char c);
void print(double d);

// 现在调用
print(1);
&lt;/code&gt;&lt;/pre&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;“人民币” (class &lt;code&gt;Money&lt;/code&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Money {
public:
    Money(double amount) : value(amount) {}
    double getValue() const { return value; }
private:
    double value;
};

void printDouble(double d) {
    std::cout &amp;lt;&amp;lt; &quot;Value: &quot; &amp;lt;&amp;lt; d &amp;lt;&amp;lt; std::endl;
}

Money myWallet(100.5);
printDouble(myWallet); // 这行代码会报错！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会报错，因为它执行了 Plan A, B, C 后发现，&lt;code&gt;printDouble&lt;/code&gt; 需要一个 &lt;code&gt;double&lt;/code&gt;，而你给了一个 &lt;code&gt;Money&lt;/code&gt; 对象。它不知道 &lt;code&gt;Money&lt;/code&gt; 和 &lt;code&gt;double&lt;/code&gt; 是什么关系。&lt;/p&gt;
&lt;p&gt;如果我们增加一个自定义转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Money {
public:
    Money(double amount) : value(amount) {}
    double getValue() const { return value; }

    // 它告诉编译器：如果你需要一个 double，而手上只有一个 Money 对象，
    operator double() const {
        return this-&amp;gt;value;
    }

private:
    double value;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>面向对象考前速通笔记 (2) 对象与类</title><link>https://fuwari.vercel.app/posts/course/ooap/ooap-note-2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/ooap/ooap-note-2/</guid><pubDate>Mon, 16 Jun 2025 09:13:06 GMT</pubDate><content:encoded>&lt;p&gt;对象（Object）、类（Class）&lt;/p&gt;
&lt;p&gt;简单来说，&lt;strong&gt;类是图纸，对象是根据图纸造出来的具体东西&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类 (Class)&lt;/strong&gt;：一个&lt;strong&gt;模板&lt;/strong&gt;或&lt;strong&gt;蓝图&lt;/strong&gt;。它描述了某一种事物 **应该有什么样的属性（特征）**和 &lt;strong&gt;能做什么样的事情（行为）&lt;/strong&gt;。比如，我们定义一个“猫”类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对象 (Object)&lt;/strong&gt;：一个&lt;strong&gt;实例 (Instance)&lt;/strong&gt;。它是根据“类”这个图纸创造出来的&lt;strong&gt;实实在在的个体&lt;/strong&gt;。比如，一只叫“大橘”的橘猫。&lt;/p&gt;
&lt;h2&gt;一. 数据抽象概述&lt;/h2&gt;
&lt;p&gt;（下面的概念会考问答题。）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;**面向对象程序设计（object-oriented programming）**是把程序构造成由若干对象组成，每个对象由一些数据以及对这些数据所能实施的操作所构成的封装体。&lt;/p&gt;
&lt;p&gt;对象的特征（数据、操作以及对外接口）由相应的类来描述，一个类所描述的对象特征可以从其他的类继承。&lt;/p&gt;
&lt;p&gt;对数据的操作通过向包含数据的对象发送消息（调用对象类对外接口中的操作）来实现。&lt;/p&gt;
&lt;p&gt;**对象（object）**是数据及对数据的操作构成的封装体。&lt;/p&gt;
&lt;p&gt;**类（class）**描述了对象的基本特征（包含哪些数据与操作）&lt;/p&gt;
&lt;p&gt;**继承（inheritance）**是指在定义一个类时，可以利用已有类的一些特征描述，即先把已有类的一些特征描述包含进来，再定义新的特征。主要涉及两个类，父类和子类，C++中叫基类和派生类。&lt;/p&gt;
&lt;p&gt;**多态性（polymorphism）**是程序设计中的一个重要概念。多态性的一般含义是：某一论域中的某个元素存在多种形式和解释。&lt;/p&gt;
&lt;p&gt;**绑定（binding）**或联编、定联，是指确定对多态元素的某个使用是多态元素的哪一种形式。绑定可分为静态绑定与动态绑定。静态绑定（static binding）也称前期绑定（early binding），是指在编译时刻确定对多态元素的使用；动态绑定（dynamic binding）也称后期绑定或延迟绑定（late binding），是指在程序运行时来确定对多&lt;/p&gt;
&lt;p&gt;态元素的使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二. 类&lt;/h2&gt;
&lt;p&gt;一个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Date
{ public:
    void set(int y, int m, int d);   // 访问控制
                                     // 成员函数
    bool is_leap_year();             // 成员函数
    void print();                    // 成员函数
    ......
private:
    int year,month,day;              // 访问控制
                                     // 数据成员
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 数据成员&lt;/h3&gt;
&lt;p&gt;数据成员的类型可以是任意的C++类型（除了&lt;code&gt;void&lt;/code&gt;）。但是在声明一个数据类型的时候，如果一个数据成员的类的定义还没有定义完，那只能用这个类型的指针或者引用类型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;为什么呢？因为数据成员的本质恰恰是需要在对象中拥有一块属于自己的、大小确定的内存空间。&lt;/p&gt;
&lt;p&gt;当定义一个类的对象的时候，比如 &lt;code&gt;Student zhangsan;&lt;/code&gt;，计算机会做一件非常重要的事情：&lt;strong&gt;为这个对象分配内存&lt;/strong&gt;。分配多大的内存呢？就是把它所有数据成员需要占用的内存加起来那么大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;void&lt;/code&gt;类型为什么不行？&lt;/strong&gt;&lt;code&gt;void&lt;/code&gt;是空的，0字节。&lt;code&gt;void&lt;/code&gt; 类型又不占任何空间。那到底该预留多大呢？0字节吗？一个不占任何空间的成员有什么意义呢？甚至无法往里面存任何东西。&lt;/p&gt;
&lt;p&gt;**为什么可以用一个没有定义过的、或定义不完整的类的指针？**因为在同一个编译环境下，所有（数据）指针的字节大小都是相同的，它大小可是确定的（比如在64位系统上是8字节）。&lt;/p&gt;
&lt;p&gt;可以通过**前向声明（Forward Declaration）**告诉编译器有这么个类，尽管定义不完整，也可以声明指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 前向声明 (Forward Declaration)
class Student;

// 现在，即使编译器还没看到 Student 的完整定义，它也可以处理 Student*
// 因为它知道所有指针的大小都一样。
class Teacher {
private:
    Student* bestStudent; // OK! 声明一个指向 Student 的指针成员

public:
    void setBestStudent(Student* s); // OK! 参数是 Student 指针
    Student* getBestStudent();       // OK! 返回值是 Student 指针
};

// 完整的类定义
// 现在我们必须提供 Student 的完整定义了，因为后面要用到它的具体成员
class Student {
public:
    string name;
    void study() { /* ... */ }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，&lt;strong&gt;一个类中的数据成员也不能是自己——完整定义自己前，你都不知道自己有多大&lt;/strong&gt;。这也循环定义了。&lt;/p&gt;
&lt;p&gt;但是搞一个自己的指针还是可以的。这也很常用。比如链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Node {
public:
    int data;     // 节点存储的数据
    Node* next;   // 指向下一个节点的指针
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;类的定义中，数据成员属于这个类的对象。在创建对象前，这里说明的数据成员并不占据内存空间。所以一般不能给它赋初值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{ int x=0;              // Error
  const double y=0.0;   // Error
......
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（课本第210页，或者6.2.1）。&lt;/p&gt;
&lt;p&gt;（但是实际上我经常这么写，没啥问题，这是基于比较旧的标准（C++98）在C++11以及之后的所有新标准，都可以直接给非静态成员赋初值了，不过这里考试还是当不行吧。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 成员函数&lt;/h3&gt;
&lt;p&gt;定义类的成员函数的具体函数体的时候，可以放在类定义中，也可以在定义外。&lt;/p&gt;
&lt;p&gt;如果放在类定义里面，就是按照内联函数处理。&lt;/p&gt;
&lt;p&gt;定义在其他地方的时候，为了和全局函数和其他的区别，要做返回值和函数名之间加上&lt;code&gt;&amp;lt;类名&amp;gt;::&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Date
{
public:
    void set(int y, int m, int d);       // 成员函数的声明
    bool is_leap_year();                // 成员函数的声明
    void print();                       // 成员函数的声明
private:
    int year,month,day;
};
void Date::set(int y, int m, int d)     // 类外定义的成员函数
{
    year = y; month = m; day = d;
}
bool Date::is_leap_year()              // 类外定义的成员函数
{
    return (year%4 == 0 &amp;amp;&amp;amp; year%100 != 0) || (year%400==0);
}
void Date::print()                    // 类外定义的成员函数
{
    cout&amp;lt;&amp;lt;year&amp;lt;&amp;lt;&quot;.&quot;&amp;lt;&amp;lt;month&amp;lt;&amp;lt;&quot;.&quot;&amp;lt;&amp;lt;day;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类成员函数名也可以重载，和全局的重载是一样的。&lt;/p&gt;
&lt;h3&gt;3. &lt;code&gt;private&lt;/code&gt;，&lt;code&gt;public&lt;/code&gt;，&lt;code&gt;protected&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;public&lt;/code&gt;：成员的访问不受限制，在程序中的任何地方都可以访问一个类的 &lt;code&gt;public &lt;/code&gt;成员。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;private&lt;/code&gt;：成员只能在本类和友元中访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;protected&lt;/code&gt;：成员只能在本类、友元和派生类中访问。（和&lt;code&gt;private&lt;/code&gt;一样，多一个派生类）&lt;/p&gt;
&lt;p&gt;C++中，类&lt;code&gt;class&lt;/code&gt;的成员默认是&lt;code&gt;private&lt;/code&gt;。结构&lt;code&gt;struct&lt;/code&gt;和联合&lt;code&gt;union&lt;/code&gt;的成员的默认是&lt;code&gt;public&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;C++ 的访问控制是基于“类”（Class-level），而不是基于“对象”（Object-level）。&lt;/p&gt;
&lt;p&gt;这句话的意思是： 只要一个函数是 &lt;code&gt;Complex&lt;/code&gt; 类的成员函数，那么它就可以访问&lt;strong&gt;任何&lt;/strong&gt; &lt;code&gt;Complex&lt;/code&gt; 对象的私有成员，而不仅仅是调用它的那个对象的私有成员。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;三. 对象&lt;/h2&gt;
&lt;h3&gt;1. 创建对象&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;直接创建对象&lt;/strong&gt;：直接定义变量。（如果是局部，就被放在栈里面，内存自动管理，结束后也不需要你手动析构，不会发生内存泄漏）（如果是全局对象，在静态存储区，或者说数据段中）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    ...
};

A a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;间接创建对象：&lt;code&gt;new&lt;/code&gt;一下。（放在堆中，手动管理内存。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A * a = new A;
// ...
delete a;	// 结束了要手动销毁
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 对象的操作&lt;/h3&gt;
&lt;p&gt;对象可以赋值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A=B&lt;/code&gt;，可以把&lt;code&gt;B&lt;/code&gt;的数据成员的值分别赋值给左边。当然也可以自己定义赋值。&lt;/p&gt;
&lt;p&gt;对象可以是做返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Date g1(const Date &amp;amp;d) {
    return d;	// 创建一个临时对象做返回值，用d对其初始化。
}

Date&amp;amp; g1(const Date &amp;amp;d) {
    return d;	// 返回d，返回值为对象的引用。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;是不是临时对象还是引用，取决于返回值的类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. &lt;code&gt;this&lt;/code&gt;指针&lt;/h3&gt;
&lt;p&gt;一个类&lt;code&gt;A&lt;/code&gt;，可能有多个对象&lt;code&gt;a1,a2,...&lt;/code&gt;，那执行一个类内函数的时候，怎么知道是对哪个类操作呢？，C++ 设计了一个巧妙的机制：&lt;strong&gt;每当成员函数被调用时，系统都会偷偷地、自动地传递一个额外的参数给这个函数，这个参数就是 &lt;code&gt;this&lt;/code&gt; 指针。&lt;/strong&gt;（类似python类中函数的第一个&lt;code&gt;self&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;大部分情况下，&lt;code&gt;this&lt;/code&gt;可以省略，但是如果成员函数的参数名和成员变量名完全相同，就要用&lt;code&gt;this&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Box {
private:
    int width;
public:
    void setWidth(int width) {
        // 如果你写 width = width; 编译器无法区分，这句代码没有意义
        // 必须用 this 来指明左边的是“本对象的成员变量width”
        this-&amp;gt;width = width; 
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 构造与析构&lt;/h3&gt;
&lt;h4&gt;构造函数&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;**构造函数（constructor）**是对象类中定义或声明的与类同名、无返回值类型的成员函数。创建对象的时候，系统会先给对象分配空间，然后用对象类的构造函数来初始化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;对构造函数的调用，属于对象创建的一部分，创建后就不能再调用了。以下是非法的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a.A();	// Error
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;不带参数的构造函数是默认构造函数（或者所有参数都有默认值的）。但是可以重载其他的构造函数。&lt;/p&gt;
&lt;p&gt;类的构造函数一般是公开的，有时也会是私有的，作用是限制该类对象的范围，这时候只能在本类或友元中创建。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;在创建动态的对象数组时，只能用默认构造函数进行初始化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;A a;
a = A(10);	// 创建了一个临时对象，并赋值给 a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面输了C++98不许直接在类定义里赋初值，那要是&lt;code&gt;const&lt;/code&gt;的数据成员怎么办？可以用成员初始化表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    int x;
    const int y;
    int &amp;amp;z;
public:
    A(): z(x), y(1) {
        x = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;A() : x(0), z(x), y(1)
        // 成员初始化表
{
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在成员初始化表中，成员初始化的书写次序并不决定它们的初始化次序，数据成员的初始化次序由它们在类定义中的说明次序决定。例如，对于上面的类 A，其数据成员的初始化次序为：x（初始化为 0）、y（初始化为 1）和 z（初始化为引用 x）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;为什么在 C++ 中需要对这个次序做出规定？&lt;/p&gt;
&lt;p&gt;我的理解是，首先，析构函数一般就一个。析构的顺序是恒定的，要按顺序的。有些成员之间有先后关系，先构造的，后析构。默认析构按照声明的顺序从后到前，那构造函数中顺序也就确定了，按照声明顺序从前到后。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果一个类&lt;code&gt;A&lt;/code&gt;的成员是一个对象&lt;code&gt;B&lt;/code&gt;。那创建&lt;code&gt;A&lt;/code&gt;的时候，也会调用&lt;code&gt;B&lt;/code&gt;的构造函数来初始化这个成员。&lt;/p&gt;
&lt;p&gt;默认是调用默认构造函数，如果要调用非默认构造函数，则需要在初始化表中显式指出。&lt;/p&gt;
&lt;h4&gt;析构函数&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;**析构函数（destructor）**是名为“&lt;code&gt;~&amp;lt;类名&amp;gt;&lt;/code&gt;”且没有参数和返回值类型的一个特殊的成员函数。当对象消亡时，在系统收回它所占的内存空间之前，会自动调用对象类的析构函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
    int x;
public:
    A();              // 构造函数
    ~A();             // 析构函数
    ......
};

......
void f()
{
    A a;             // 调用 a 的构造函数 A()
    ......
}                  // 调用 a 的析构函数 ~A()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，在撤销动态对象数组时，如果没写 &lt;code&gt;delete&lt;/code&gt; 操作中的 “&lt;code&gt;[]&lt;/code&gt;”，则只有数组中的第一个对象的析构函数会被调用。&lt;/p&gt;
&lt;p&gt;然后类里面&lt;code&gt;new&lt;/code&gt;的要自己去释放空间。&lt;/p&gt;
&lt;p&gt;一个类&lt;code&gt;A&lt;/code&gt;，如果数据成员有类&lt;code&gt;B,C&lt;/code&gt;，怎么析构？假设是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
private:
    B b_member;  // b_member 在前
    C c_member;  // c_member 在后

public:
    A() {
        // ... A 构造函数的代码 ...
    }
    ~A() {
        // ... A 析构函数的代码 ...
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造顺序是&lt;code&gt;B-&amp;gt;C&lt;/code&gt;，构造完后，才会调用&lt;code&gt;A()&lt;/code&gt;构造出&lt;code&gt;A&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;析构的顺序反之。先执行&lt;code&gt;~A()&lt;/code&gt;到结束，然后再调用&lt;code&gt;~C()&lt;/code&gt;,&lt;code&gt;~B()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果没有提供析构函数的类，如果它有成员对象，则编译程序会隐式地为之提供一个析构函数，该析构函数的作用是调用成员对象类的析构函数（但这并不是说直接写在这个函数里的，这里析构函数其实是空的&lt;code&gt;{}&lt;/code&gt;，作用就是直接跳过，到析构成员这一步）&lt;/p&gt;
&lt;h3&gt;5. 拷贝构造函数&lt;/h3&gt;
&lt;p&gt;就和默认构造、析构函数一样，拷贝构造函数，也是编译器会自动生成的一种函数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;在创建一个对象时，如果用另一个同类的对象对其进行初始化，将会调用一个特殊的构造函数——&lt;strong&gt;在创建一个对象时，如果用另一个同类的对象对其进行初始化，将会调用一个特殊的构造函数——拷贝构造函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    A();
    A(const A&amp;amp; a);	// 拷贝构造函数 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;要点：创建一个对象时、用另一个同类对象初始化，才会调用拷贝构造函数。&lt;/p&gt;
&lt;p&gt;它的本质是一个&lt;strong&gt;构造函数&lt;/strong&gt;，特殊之处在于它的&lt;strong&gt;参数&lt;/strong&gt;是同一个类的另一个对象。&lt;/p&gt;
&lt;p&gt;直接看&lt;code&gt;A(const A&amp;amp; a);&lt;/code&gt;就能看出来了：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A(&lt;/code&gt; ... &lt;code&gt;)&lt;/code&gt;: 这说明它是一个构造函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A&amp;amp; a&lt;/code&gt;: 参数 &lt;code&gt;a&lt;/code&gt; 是对源对象（被拷贝的对象）的一个&lt;strong&gt;引用 (&lt;code&gt;&amp;amp;&lt;/code&gt;)&lt;/strong&gt;。这样做是为了效率，避免在传递参数时又发生一次拷贝。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;: &lt;code&gt;const&lt;/code&gt; 关键字告诉编译器，在拷贝过程中，我们保证&lt;strong&gt;不会修改&lt;/strong&gt;源对象 &lt;code&gt;a&lt;/code&gt;。这是一个好习惯，也更安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;什么时候会调用拷贝构造函数？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况 1，直接初始化。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a1;           // 调用默认构造函数，造出房子 a1
A a2(a1);       // 克隆 a1，造出 a2。明确调用拷贝构造函数。
A a3 = a1;      // **注意！** 这也是克隆，不是赋值！因为 a3 是在定义时初始化的。
A a4 = A(a1);	// 这也是拷贝构造函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;情况 2：把对象作为值参数传递给函数。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f(A x);  // 函数 f 接受一个 A 类型的参数 x
...
A a;          // 造一个对象 a
f(a);         // 把 a 传给函数 f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;f(a)&lt;/code&gt; 被调用时，程序并不是把 &lt;code&gt;a&lt;/code&gt; 本身送进函数。为了保护原始的 &lt;code&gt;a&lt;/code&gt; 不被函数内部意外修改，C++ 会在函数 &lt;code&gt;f&lt;/code&gt; 的内部创建一个&lt;strong&gt;临时副本&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;，然后用 &lt;code&gt;a&lt;/code&gt; 来&lt;strong&gt;初始化&lt;/strong&gt;这个 &lt;code&gt;x&lt;/code&gt;。这个过程，就是通过调用拷贝构造函数完成的。&lt;code&gt;x&lt;/code&gt; 是 &lt;code&gt;a&lt;/code&gt; 的一个克隆品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况 3：函数按值返回一个对象。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A f() {
    A a;
    ...
    return a; // 函数要返回一个 A 类型的对象
}
...
A b = f();    // 用函数的返回值来初始化 b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当函数 &lt;code&gt;f&lt;/code&gt; 执行到 &lt;code&gt;return a;&lt;/code&gt; 时，程序需要把 &lt;code&gt;a&lt;/code&gt; 的值带出函数。它会创建一个&lt;strong&gt;临时的、匿名的对象&lt;/strong&gt;，并用 &lt;code&gt;a&lt;/code&gt; 来初始化这个临时对象（调用拷贝构造函数）。然后，这个临时对象再被用来初始化外面的 &lt;code&gt;b&lt;/code&gt;（再次调用拷贝构造函数）。&lt;/p&gt;
&lt;p&gt;如果不自己写拷贝构造函数，编译器会&lt;strong&gt;自动生成一个&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个默认的拷贝构造函数做的事情很简单，叫做**“逐个成员初始化” (Member-wise Initialization)**。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于普通数据类型&lt;/strong&gt; (如 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;)：直接复制值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于成员对象&lt;/strong&gt; (一个类包含另一个类的对象)：调用那个成员对象的拷贝构造函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;深拷贝 vs. 浅拷贝&lt;/h4&gt;
&lt;p&gt;在绝大多数情况下都不需要自己实现，隐式的拷贝构造函数够用了，但是有些情况有问题！&lt;/p&gt;
&lt;p&gt;什么情况有问题？直接复制值，如果有个成员存的是指针，并且指针在初始化中才用&lt;code&gt;new&lt;/code&gt;创建的对象。那这里复制一份后，指针并没有重新指向新的&lt;code&gt;new&lt;/code&gt;的对象，而是指向了原来那个对象的成员！&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
    int x,y;
    char *p;
public:
    A(char *str)
    {
        x = 0;
        y = 0;
        p = new char[strlen(str)+1];
        strcpy(p,str);
    }
    ~A() { delete [ ]p; p = NULL; }
    ......
    // 其中没有定义拷贝构造函数
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就有问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A a1(&quot;abcd&quot;);
A a2(a1);    // 调用隐式拷贝构造函数，用 a1 对 a2 进行初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，隐式拷贝构造函数实现的，我们称呼它为&lt;strong&gt;浅拷贝（shallow copy）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;解决办法就是显式定义一个拷贝构造函数指针，来实现&lt;strong&gt;深拷贝（deep copy）&lt;/strong&gt;。为新的对象的指针分配指向的空间，然后把牢对象指针指向的成员复制过来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一旦你决定自己写拷贝构造函数，你就要对所有成员的拷贝行为负全责！编译器不会再像以前那么“智能”地帮你处理了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;编译器默认的拷贝构造函数，对于拷贝对象的时候，调用的是A的拷贝构造函数。如果自己写，在拷贝构造函数中如果没有明确指出&lt;code&gt;a&lt;/code&gt;是怎么做的，编译器就会调用默认构造函数&lt;code&gt;A()&lt;/code&gt;来创建&lt;code&gt;A&lt;/code&gt;，有可能就不是真的拷贝了。&lt;/p&gt;
&lt;p&gt;一个正确的实现是这样的：使用&lt;strong&gt;成员初始化列表 (Member Initializer List)&lt;/strong&gt; 来实现这个拷贝：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B(const B&amp;amp; b) : a(b.a) { 
    z = b.z; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四. 进一步讨论&lt;/h2&gt;
&lt;h3&gt;1. 常成员函数（&lt;code&gt;const&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;一个对象（比如一个“银行账户”对象）通常有两种基本操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;修改性操作&lt;/strong&gt;：改变对象的状态。例如 &lt;code&gt;deposit()&lt;/code&gt; (存款) 或 &lt;code&gt;withdraw()&lt;/code&gt; (取款) 会改变账户的 &lt;code&gt;balance&lt;/code&gt; (余额) 成员变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取性操作&lt;/strong&gt;：只读取对象的状态，不进行任何修改。例如 &lt;code&gt;getBalance()&lt;/code&gt; (查询余额) 或 &lt;code&gt;getAccountHolder()&lt;/code&gt; (获取户主姓名)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在编程时，我们非常希望能在语法层面就严格区分这两种操作。如果一个函数我们&lt;strong&gt;期望&lt;/strong&gt;它只是“查询余额”，但因为疏忽在里面写了&lt;code&gt;balance++&lt;/code&gt;的代码，这就会造成一个难以发现的 bug。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，C++ 提供了&lt;strong&gt;常成员函数&lt;/strong&gt;机制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;在成员函数的声明和定义的末尾（括号后面，分号或大括号前面）加上 &lt;code&gt;const&lt;/code&gt; 关键字，这个函数就变成了&lt;strong&gt;常成员函数&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    int get_value() const; // 声明一个常成员函数
    void set_value(int v); // 一个普通的成员函数
private:
    int value;
};

// 在类外定义时也要加上 const
int A::get_value() const {
    // value = 10; // 错误！不可以在常成员函数内修改成员变量
    return value;  // 正确，只读取不修改
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt; 关键字就像一个&lt;strong&gt;承诺&lt;/strong&gt;和一把&lt;strong&gt;锁&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对编译器的承诺&lt;/strong&gt;：你告诉编译器：“我保证这个函数绝对不会修改对象的任何成员变量。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译器提供的锁&lt;/strong&gt;：编译器会严格监督你的承诺。一旦它发现在常成员函数内部有任何试图修改成员变量的行为（如书上的 &lt;code&gt;x++&lt;/code&gt;），它会立刻报错，从而在编译阶段就帮你发现了潜在的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;指针问题&lt;/h4&gt;
&lt;p&gt;但是有一个非常微妙但致命的“陷阱”：&lt;code&gt;const&lt;/code&gt; 保证的是&lt;strong&gt;成员变量本身&lt;/strong&gt;不被修改，但&lt;strong&gt;管不了&lt;/strong&gt;成员变量（如果是个指针）所&lt;strong&gt;指向的内存&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    int x;
    char *p;
public:
    void f() const {
        x = 10;           // 错误！const 锁住了 x，不能修改。
        p = new char[20]; // 错误！const 锁住了指针 p 本身，不能让它指向新的内存地址。

        strcpy(p, &quot;abcd&quot;); // 正确！编译器允许这么做。
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个 C++ 语法的局限性。必须由写代码的人保证，在常成员函数中，不仅不修改指针成员变量本身，也不要通过它去修改它所指向的外部数据。&lt;/p&gt;
&lt;h4&gt;常量对象&lt;/h4&gt;
&lt;p&gt;什么是常量对象？就是一个在定义后就不希望被修改的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const A my_const_object; // 一个常量对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++ 规定：&lt;strong&gt;对于一个常量对象，你只能调用它的常成员函数，绝对不能调用任何普通（非 const）的成员函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在写函数时，我们经常把参数定义为&lt;strong&gt;常量引用 (&lt;code&gt;const A&amp;amp;&lt;/code&gt;)&lt;/strong&gt; 或&lt;strong&gt;常量指针 (&lt;code&gt;const A\*&lt;/code&gt;)&lt;/strong&gt;。这样做有两个好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;效率高&lt;/strong&gt;：避免了像 &lt;code&gt;(A a)&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;code&gt;const&lt;/code&gt;，就意味着在这个函数里，你只能调用该参数的&lt;strong&gt;常成员函数&lt;/strong&gt;了。所以，把所有“只读”操作的函数都定义为常成员函数，是一个非常好的编程习惯。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    void f() const { ... } // 常成员函数
    void g() { ... }       // 普通成员函数
};

// func 的参数是一个指向常量对象的指针
// 这意味着 func 承诺不会通过 pa 来修改它所指向的那个 A 对象
void func(const A* pa) {
    pa-&amp;gt;f();  // 正确！因为 f() 是 const 函数，可以被常量对象调用。
    pa-&amp;gt;g();  // 错误！因为 g() 是普通函数，编译器担心它会修改对象，所以禁止调用。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;在C++中，&lt;code&gt;const&lt;/code&gt; 关键字修饰的是它&lt;strong&gt;左边&lt;/strong&gt;的东西（如果左边没有，就修饰右边的）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const A* pa&lt;/code&gt; 的意思是：“&lt;strong&gt;pa 是一个指向常量A的指针&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A* const pa&lt;/code&gt; 的意思是，常量指针。&lt;code&gt;const&lt;/code&gt; 在 &lt;code&gt;*&lt;/code&gt; 的左边，所以它修饰的是 &lt;code&gt;*&lt;/code&gt;，但为了方便理解，我们通常认为它修饰的是 &lt;code&gt;pa&lt;/code&gt; 本身。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 静态数据成员（&lt;code&gt;static&lt;/code&gt;）&lt;/h3&gt;
&lt;h4&gt;静态数据成员&lt;/h4&gt;
&lt;p&gt;有的时候会有需求，我们想让一些数据被这个类的所有对象共享。&lt;/p&gt;
&lt;p&gt;用全局变量也能实现数据共享，但那样做会破坏类的封装性，任何人都能随意修改，很不安全。静态成员就是为了解决这个问题而生的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态数据成员 (Static Data Member)&lt;/strong&gt;，在类的内部，用 &lt;code&gt;static&lt;/code&gt; 关键字声明的成员变量。&lt;/p&gt;
&lt;/blockquote&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;：无论你创建了 0 个还是 100 个对象，静态数据成员在内存中永远只有一份。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在类外定义和初始化&lt;/strong&gt;：这是一个非常独特的语法规则。你必须在类的定义之外，对静态数据成员进行定义和初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class A {
    int x, y; // 普通成员变量，每个对象都有一份
    static int shared; // 静态数据成员，所有对象共享一份

public:
    void increase_all() { x++; y++; shared++; }
    int sum_all() const { return x+y+shared; }
    // ...
};

// **关键步骤**：在类外进行定义和初始化
int A::shared = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;static int shared;&lt;/code&gt;：这只是一个&lt;strong&gt;声明&lt;/strong&gt;，告诉编译器：“A 这个类里有一个叫 &lt;code&gt;shared&lt;/code&gt; 的静态整数成员。”&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int A::shared = 0;&lt;/code&gt;：这才是&lt;strong&gt;定义&lt;/strong&gt;和&lt;strong&gt;初始化&lt;/strong&gt;。它在内存中为 &lt;code&gt;shared&lt;/code&gt; 分配了空间，并把它初始值设为 0。&lt;code&gt;A::&lt;/code&gt; 表明这个 &lt;code&gt;shared&lt;/code&gt; 是属于类 &lt;code&gt;A&lt;/code&gt; 的。&lt;strong&gt;这一行绝对不能省略！&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;静态成员函数&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态成员函数 (Static Member Function)&lt;/strong&gt;，是用 &lt;code&gt;static&lt;/code&gt; 关键字声明的成员函数。&lt;/p&gt;
&lt;/blockquote&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;code&gt;this&lt;/code&gt; 指针&lt;/strong&gt;：普通成员函数在被调用时，都有一个隐藏的 &lt;code&gt;this&lt;/code&gt; 指针，指向调用它的那个对象。但静态成员函数属于类，不属于任何特定对象，所以它&lt;strong&gt;没有 &lt;code&gt;this&lt;/code&gt; 指针&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问限制&lt;/strong&gt;：因为没有 &lt;code&gt;this&lt;/code&gt; 指针，它&lt;strong&gt;不能直接访问&lt;/strong&gt;任何非静态成员（无论是变量还是函数）。它只能访问&lt;strong&gt;其他静态成员&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;静态成员函数有两种调用方式，但更推荐第二种，因为它更清晰地体现了“属于类”的本质。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 通过对象调用 (不推荐)
A a;
a.set_shared(10); 

// 2. 通过类名和作用域解析符::调用 (推荐)
A::set_shared(20);
cout &amp;lt;&amp;lt; A::get_shared() &amp;lt;&amp;lt; endl; // 输出 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 友元&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;友元是 C++ 提供的一种机制，它允许一个类主动声明外部的某个&lt;strong&gt;特定函数&lt;/strong&gt;或&lt;strong&gt;特定类&lt;/strong&gt;是它的“朋友”，从而授权这个“朋友”可以直接访问该类的 &lt;code&gt;private&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员。这是一种在&lt;strong&gt;数据保护&lt;/strong&gt;和&lt;strong&gt;访问效率&lt;/strong&gt;之间的&lt;strong&gt;折中方案&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;友元函数&lt;/h4&gt;
&lt;p&gt;举例子，矩阵乘法的全局函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 不用友元的、低效的实现方式
Vector multiply(const Matrix&amp;amp; m, const Vector&amp;amp; v) {
    // ... 检查维度是否匹配 ...
    Vector result(m.dimension_row());
    for (int i = 0; i &amp;lt; m.dimension_row(); ++i) {
        int sum = 0;
        for (int j = 0; j &amp;lt; m.dimension_column(); ++j) {
            // 每次访问元素都要通过函数调用，效率较低
            sum += m.element(i, j) * v.element(j);
        }
        result.element(i) = sum;
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码需要反复调用 &lt;code&gt;element()&lt;/code&gt; 函数，这在计算密集型任务中会带来不必要的开销。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;友元解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们可以把 &lt;code&gt;multiply&lt;/code&gt; 函数声明为 &lt;code&gt;Matrix&lt;/code&gt; 类和 &lt;code&gt;Vector&lt;/code&gt; 类的&lt;strong&gt;友元&lt;/strong&gt;。这样，它就获得了直接访问这两个类私有数据的“特权”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// --- 在 Vector.h (或类似文件) ---
class Vector {
    // ... 其他成员 ...
    // 声明全局函数 multiply 是我的朋友
    friend Vector multiply(const Matrix&amp;amp; m, const Vector&amp;amp; v);
private:
    int *p_data;
    int num;
};

// --- 在 Matrix.h (或类似文件) ---
// 因为 Matrix 的友元函数 multiply 用到了 Vector，所以需要前向声明 Vector 类
class Vector; // 前向声明

class Matrix {
    // ... 其他成员 ...
    // 声明全局函数 multiply 是我的朋友
    friend Vector multiply(const Matrix&amp;amp; m, const Vector&amp;amp; v);
private:
    int *p_data;
    int row, col;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，&lt;code&gt;multiply&lt;/code&gt; 函数可以直接使用 &lt;code&gt;.&lt;/code&gt; 操作符访问私有成员，就像它是类的成员函数一样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// --- 在 multiply.cpp (或类似文件) ---
Vector multiply(const Matrix&amp;amp; m, const Vector&amp;amp; v) {
    // ... 检查维度是否匹配，例如 if (m.col != v.num) ...

    Vector result(m.row); // 创建结果向量

    for (int i = 0; i &amp;lt; m.row; ++i) {
        int sum = 0;
        for (int j = 0; j &amp;lt; m.col; ++j) {
            // 因为是友元，所以可以高效地直接访问私有数据！
            sum += m.p_data[i * m.col + j] * v.p_data[j];
        }
        result.p_data[i] = sum; // 同样可以直接访问 result 的私有数据
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;friend&lt;/code&gt; 声明放在类的 &lt;code&gt;public&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt; 还是 &lt;code&gt;protected&lt;/code&gt; 部分，其效果是&lt;strong&gt;完全一样&lt;/strong&gt;的。编译器并不关心你把它放在哪里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;友元类&lt;/h4&gt;
&lt;p&gt;我们刚才讲的是&lt;strong&gt;友元函数 (friend function)&lt;/strong&gt;，是把访问权限授予一个&lt;strong&gt;全局函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;friend class B;&lt;/code&gt; 则是另一种形式，叫做&lt;strong&gt;友元类 (friend class)&lt;/strong&gt;。它的意思是&lt;strong&gt;将访问权限授予一整个类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果在 &lt;code&gt;A&lt;/code&gt; 类中声明 &lt;code&gt;friend class B;&lt;/code&gt;，就意味着 &lt;code&gt;B&lt;/code&gt; 类的&lt;strong&gt;所有成员函数&lt;/strong&gt;都可以直接访问 &lt;code&gt;A&lt;/code&gt; 类的 &lt;code&gt;private&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员。&lt;/p&gt;
&lt;p&gt;最后，我们回顾一下的友元特性，这很重要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;友元非成员&lt;/strong&gt;：友元函数不属于这个类，它只是被开了个“后门”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不具对称性&lt;/strong&gt;：如果在 &lt;code&gt;A&lt;/code&gt; 类中声明 &lt;code&gt;B&lt;/code&gt; 是朋友 (&lt;code&gt;friend class B;&lt;/code&gt;)，不代表 &lt;code&gt;A&lt;/code&gt; 也是 &lt;code&gt;B&lt;/code&gt; 的朋友。授权是单向的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不具传递性&lt;/strong&gt;：&lt;code&gt;A&lt;/code&gt; 是 &lt;code&gt;B&lt;/code&gt; 的朋友，&lt;code&gt;B&lt;/code&gt; 是 &lt;code&gt;C&lt;/code&gt; 的朋友，不代表 &lt;code&gt;A&lt;/code&gt; 就是 &lt;code&gt;C&lt;/code&gt; 的朋友。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. 转移构造函数*&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;P240&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5. 操作符重载&lt;/h3&gt;
&lt;p&gt;数学上看，操作符就是一种函数，其中，操作数是函数的参数，结果是函数的返回值。&lt;/p&gt;
&lt;p&gt;在C++中，操作符重载（operator overloading）是通过函数实现的。&lt;/p&gt;
&lt;p&gt;定义一个函数，以&lt;code&gt;operator &amp;lt;操作符&amp;gt;&lt;/code&gt;的函数。&lt;/p&gt;
&lt;p&gt;可以写成成员函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Complex {
    double real, imag;
public:
    Complex operator + (const Complex&amp;amp; x) const {
        Complex temp;
        temp.imag = imag + x.imag;
        return temp;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以作为全局函数，然后变成友元。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Complex {
private:
    double real, imag;
public:   
    fridend Complex operator + (const Complex&amp;amp; x1, const Complex&amp;amp; x2);
};

Complex operator + (const Complex&amp;amp; x1, const Complex&amp;amp; x2) {
    Complex temp;
    temp.real = x1.real + x2.real;
    temp.imag = x1.imag + x2.imag;
    return temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了&lt;code&gt;.&lt;/code&gt;,&lt;code&gt;.*&lt;/code&gt;,&lt;code&gt;::&lt;/code&gt;,&lt;code&gt;?:&lt;/code&gt;,&lt;code&gt;sizeof&lt;/code&gt;，其他都可以重载。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;=&lt;/code&gt;重载&lt;/h4&gt;
&lt;p&gt;默认的对象赋值，&lt;code&gt;A=B&lt;/code&gt;，其实是有一个隐的赋值操作符重载函数的，&lt;/p&gt;
&lt;p&gt;和前面的拷贝构造函数一样，这里也有深拷贝和浅拷贝的问题。而且这里还不能构造，得先处理自己的内存问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A&amp;amp; A::operator=(const A&amp;amp; a) {
    // 1. 防止自我赋值 (a = a;)
    if (this == &amp;amp;a) {
        return *this;
    }

    // 2. 释放自己原来的内存空间
    delete [] p;

    // 3. 根据对方的大小，为自己申请一块新的内存
    p = new char[strlen(a.p) + 1];

    // 4. 将对方内存中的“内容”复制到自己的新内存中
    strcpy(p, a.p);

    // 5. 复制其他非指针成员
    x = a.x;
    y = a.y;

    // 6. 返回对自己（左值）的引用，以支持链式赋值 (a = b = c;)
    return *this;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;[]&lt;/code&gt;重载&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class String {
    char *p;
public:
    // ...
    char&amp;amp; operator[](int i) { // 操作符 &quot;[]&quot; 的重载函数
        // 增加安全性检查，这是比普通数组更优秀的地方
        if (i &amp;gt;= strlen(p) || i &amp;lt; 0) {
            cerr &amp;lt;&amp;lt; &quot;下标越界错误\n&quot;;
            exit(-1);
        }
        return p[i];
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>面向对象考前速通笔记 (3) 继承、模板</title><link>https://fuwari.vercel.app/posts/course/ooap/ooap-note-3/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/ooap/ooap-note-3/</guid><pubDate>Mon, 16 Jun 2025 09:13:05 GMT</pubDate><content:encoded>&lt;h2&gt;一. 单继承&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;**单继承（single inheritance）**是指一个派生类只有一个直接基类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;C++ 允许一个类（&lt;strong&gt;派生类&lt;/strong&gt;）继承另一个类（&lt;strong&gt;基类&lt;/strong&gt;）的功能，从而实现代码复用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class 派生类名 : &amp;lt;继承方式&amp;gt; 基类名 {
    // ... 派生类新增的成员
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;继承方式&amp;gt;&lt;/code&gt;&lt;/strong&gt; 可以是 &lt;code&gt;public&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt; 或 &lt;code&gt;protected&lt;/code&gt;（也可以省略，就是默认&lt;code&gt;private&lt;/code&gt;）。这个关键字非常重要，它决定了基类成员在派生类中的&lt;strong&gt;访问权限&lt;/strong&gt;。如果省略，默认为 &lt;code&gt;private&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;派生类会自动获得基类的所有成员（成员变量和成员函数），但&lt;strong&gt;不包括&lt;/strong&gt;基类的构造函数、析构函数和赋值运算符重载。&lt;/p&gt;
&lt;p&gt;如果派生类定义了和基类同名的成员，也就是重定义。不会报错，之前那个就没有用了，如果要访问之前那个，要&lt;code&gt;基类::成员名&lt;/code&gt;来访问。如果成员是函数，也是类似的，即使参数不同，还是要指定基类才能访问。&lt;/p&gt;
&lt;p&gt;（这不是函数名重载，因为不属于同一个作用域）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A // 基类
{
    int x, y;
public:
    void f();
    void g();
};

class B: public A // 派生类
{
    int z; // 新定义的数据成员
public:
    void h(); // 新定义的成员函数
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;B&lt;/code&gt;，也包含&lt;code&gt;A&lt;/code&gt;中的&lt;code&gt;x,y,f,g&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以把派生类的对象理解成它包含了基类的一个子对象，该子对象的内存空间位于派生类对象内存空间的前部。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;如果没有在派生类中显式说明，则基类的友元不是派生类的友元；如果基类是另一个类的友元，而在该类中没有显式说明，则派生类也不是该类的友元。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;到这里就该疑问了，&lt;code&gt;B&lt;/code&gt;有是有了&lt;code&gt;A&lt;/code&gt;的一切成员，那这些继承的成员的权限呢？会变化吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;会变！&lt;/strong&gt; 成员被继承后的最终权限，由&lt;strong&gt;它在基类中的原始权限&lt;/strong&gt;和&lt;strong&gt;继承方式&lt;/strong&gt;两者&lt;strong&gt;共同决定&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 继承方式&lt;/h3&gt;
&lt;p&gt;可以把“继承方式”想象成一个&lt;strong&gt;权限过滤器&lt;/strong&gt;或一个&lt;strong&gt;总开关&lt;/strong&gt;。它&lt;strong&gt;限制&lt;/strong&gt;了基类成员在派生类中&lt;strong&gt;最高能拥有什么权限&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt; 继承：最开放的继承方式。它告诉编译器：“基类的成员权限&lt;strong&gt;基本保持不变&lt;/strong&gt;。”（具体规则见下表）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected&lt;/code&gt; 继承：一个更严格的开关。它告诉编译器：“基类中所有 &lt;code&gt;public&lt;/code&gt; 的成员，到了我这里（派生类）&lt;strong&gt;最高也只能是 &lt;code&gt;protected&lt;/code&gt;&lt;/strong&gt;。”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt; 继承：最严格的继承方式。它告诉编译器：“基类中所有 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 的成员，到了我这里&lt;strong&gt;全部都变成 &lt;code&gt;private&lt;/code&gt;&lt;/strong&gt;。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以用一个表格清晰地展示这个规则：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;基类中的原始权限&lt;/th&gt;
&lt;th&gt;&lt;code&gt;public&lt;/code&gt; 继承&lt;/th&gt;
&lt;th&gt;&lt;code&gt;protected&lt;/code&gt; 继承&lt;/th&gt;
&lt;th&gt;&lt;code&gt;private&lt;/code&gt; 继承&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;public&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;private&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不可访问&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不可访问&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不可访问&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;特别注意&lt;/strong&gt;：无论采用哪种继承方式，&lt;strong&gt;基类的 &lt;code&gt;private&lt;/code&gt; 成员永远只能被基类自己访问，派生类永远无法直接访问&lt;/strong&gt;。虽然派生类的对象里确实包含了这个成员（占用了内存空间），但在语法上就是不能碰。&lt;/p&gt;
&lt;h4&gt;继承方式的调整&lt;/h4&gt;
&lt;p&gt;但是继承方式是可以调整的。&lt;/p&gt;
&lt;p&gt;这是一个相对不常用但需要了解的技巧，主要用在 &lt;code&gt;private&lt;/code&gt; 或 &lt;code&gt;protected&lt;/code&gt; 继承中。我们之前学到，用 &lt;code&gt;private&lt;/code&gt; 方式继承时，基类所有的 &lt;code&gt;public&lt;/code&gt; 和 &lt;code&gt;protected&lt;/code&gt; 成员在派生类中都会变成 &lt;code&gt;private&lt;/code&gt;。但如果我就是想让其中一两个成员重新变回 &lt;code&gt;public&lt;/code&gt; 或 &lt;code&gt;protected&lt;/code&gt;，怎么办？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;/strong&gt;：在派生类对应的权限区（&lt;code&gt;public&lt;/code&gt; 或 &lt;code&gt;protected&lt;/code&gt;）中，使用 &lt;code&gt;基类名::成员名;&lt;/code&gt; 的语法，可以“恢复”该成员的访问权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    void f1(), f2(), f3();
protected:
    void g1(), g2(), g3();
};

class B : private A { // 采用 private 继承，A的所有成员在B中默认都是private
public:
    A::f1;  // 把原本应为 private 的 f1，权限调整回 public
    A::g1;  // 把原本应为 private 的 g1，权限调整为 public
protected:
    A::f2;  // 把原本应为 private 的 f2，权限调整为 protected
    A.g2;  // 把原本应为 private 的 g2，权限调整为 protected

// f3 和 g3 没有被提及，所以它们在 B 中仍然是 private
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重要限制&lt;/strong&gt;：你不能通过这种方式让成员的权限变得比它在基类中更宽松。比如，你不能把基类中的 &lt;code&gt;protected&lt;/code&gt; 成员 &lt;code&gt;g1&lt;/code&gt; 在派生类里调整为 &lt;code&gt;public&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;子类型关系&lt;/h4&gt;
&lt;p&gt;“&lt;code&gt;public&lt;/code&gt; 继承方式有着特殊的意义，它将子类型（subtype）用作基类的对外接口”。&lt;/p&gt;
&lt;p&gt;意思就是：如果 &lt;code&gt;class B&lt;/code&gt; 公开继承（&lt;code&gt;public&lt;/code&gt;）了 &lt;code&gt;class A&lt;/code&gt;，那么就意味着 &lt;code&gt;B&lt;/code&gt; 就是一种 &lt;code&gt;A&lt;/code&gt;。你可以把派生类当作基类来用，但反过来不行。&lt;/p&gt;
&lt;p&gt;什么意思呢？举个例子，&lt;code&gt;class B : public A&lt;/code&gt;，所以一个 &lt;code&gt;B&lt;/code&gt; 对象&lt;strong&gt;就是&lt;/strong&gt;一个 &lt;code&gt;A&lt;/code&gt; 对象（并且还带有额外功能）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;合法的操作（把派生类当基类用 - Upcasting）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就像说“一只哈士奇（派生类）可以被当作一只狗（基类）来看待”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;A *p = &amp;amp;b;&lt;/code&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;code&gt;a = b;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;：可以将派生类对象赋值给基类对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释&lt;/strong&gt;：这个操作叫做&lt;strong&gt;对象切片 (Slicing)&lt;/strong&gt;。它会把 &lt;code&gt;b&lt;/code&gt; 对象中从 &lt;code&gt;A&lt;/code&gt; 继承来的那一部分“切”下来，拷贝给 &lt;code&gt;a&lt;/code&gt;。&lt;code&gt;b&lt;/code&gt; 自己独有的成员（如 &lt;code&gt;int z;&lt;/code&gt;）会被忽略掉。就像你把一只“哈士奇”的信息登记到“狗”的档案里，你只会记录犬类共有的信息（姓名、年龄），而不会记录哈士奇独有的“拆家指数”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func1(&amp;amp;b); func2(b); func3(b);&lt;/code&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;/ol&gt;
&lt;p&gt;&lt;strong&gt;非法的操作（把基类当派生类用 - Downcasting）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就像说“你不能把一只普通的狗（基类）当作一只哈士奇（派生类）”，因为它不具备哈士奇的全部特征。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;B *q = &amp;amp;a;&lt;/code&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;code&gt;z&lt;/code&gt;）。如果你强行这么做，然后通过指针 &lt;code&gt;q&lt;/code&gt; 去访问 &lt;code&gt;z&lt;/code&gt; (&lt;code&gt;q-&amp;gt;z&lt;/code&gt;)，程序就会出错。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b = a;&lt;/code&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;code&gt;z&lt;/code&gt;）该填什么呢？基类对象 &lt;code&gt;a&lt;/code&gt; 根本没有这个数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func4(&amp;amp;a); func5(a); func6(a);&lt;/code&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;/ol&gt;
&lt;h3&gt;2. 派生类对象的初始化和消亡&lt;/h3&gt;
&lt;h4&gt;构造函数&lt;/h4&gt;
&lt;p&gt;派生类对象的初始化，是由基类和派生类共同完成的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;基类的数据成员，由基类的构造函数初始化。默认情况下调用基类的默认构造函数。如果要调用基类的非默认函数，就必须在派生类的成员初始化表中指出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
    int x;
public:
    A() { x = 0; }
    A(int i) { x = i; }
};
class B: public A
{
    int y;
public:
    B()           // 将调用 A 的默认构造函数 A()
    { y = 0; }
    B(int i)      // 将调用 A 的默认构造函数 A()
    { y = i; }
    B(int i, int j):A(i) // 将调用 A 的构造函数 A(int i)
    { y = j; }
};

// ...
B b1;            // 调用 A::A() 和 B::B()，b1.x 等于 0，b1.y 等于 0
B b2(1);         // 调用 A::A(int) 和 B::B(int)，b2.x 等于 0，b2.y 等于 1
B b3(1,2);       // 调用 A::A(int) 和 B::B(int,int)，b3.x 等于 1，b3.y 等于 2

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;派生类的数据成员，由派生类的构造函数初始化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;拷贝构造函数&lt;/h4&gt;
&lt;p&gt;如果派生类没写任何拷贝构造函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 你没有为 B 写拷贝构造函数
B b1( ... );
B b2 = b1; // 编译器自动工作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器知道你想干什么。你想完整地复制一个 &lt;code&gt;b1&lt;/code&gt; 到 &lt;code&gt;b2&lt;/code&gt;。一个完整的 &lt;code&gt;B&lt;/code&gt; 对象包含两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从基类 &lt;code&gt;A&lt;/code&gt; 继承来的部分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;B&lt;/code&gt; 自己独有的部分。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是，智能的编译器会自动这么做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于基类 &lt;code&gt;A&lt;/code&gt; 的部分&lt;/strong&gt;：调用 &lt;code&gt;A&lt;/code&gt; 的&lt;strong&gt;拷贝构造函数&lt;/strong&gt; &lt;code&gt;A(const A&amp;amp;)&lt;/code&gt; 来完成拷贝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于派生类 &lt;code&gt;B&lt;/code&gt; 的部分&lt;/strong&gt;：逐个拷贝 &lt;code&gt;B&lt;/code&gt; 的成员变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果自己实现了拷贝构造函数。问题来了，一切都要我们自己负责。&lt;/p&gt;
&lt;p&gt;“在任何派生类的构造函数中，如果你没有在成员初始化列表里明确地告诉我要如何初始化基类 &lt;code&gt;A&lt;/code&gt;，那我就只能调用 &lt;code&gt;A&lt;/code&gt; 的默认（无参）构造函数 &lt;code&gt;A()&lt;/code&gt;。” 这个规则是铁律，它不管你当前写的是不是“拷贝构造函数”。只要你写了构造函数，就得遵守这个规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class B : public A {
public:
    // ...
    // 你写的“手动挡”拷贝构造函数
    B(const B&amp;amp; other) {
        // 函数体内部
        this-&amp;gt;y = other.y; // 你只告诉了编译器如何拷贝 B 自己的成员
    }
private:
    int y;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里你&lt;strong&gt;没有&lt;/strong&gt;在成员初始化列表里给编译器任何关于如何处理基类 &lt;code&gt;A&lt;/code&gt; 的指令。比如，你没有写 &lt;code&gt;: A(other)&lt;/code&gt;。编译器遵守铁律，自动在背后调用了 &lt;code&gt;A&lt;/code&gt; 的&lt;strong&gt;默认构造函数 &lt;code&gt;A()&lt;/code&gt;&lt;/strong&gt;。然后，才进入你的函数体，执行 &lt;code&gt;this-&amp;gt;y = other.y;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这几乎肯定是一个BUG。A的值包没有赋值进去的。&lt;/p&gt;
&lt;p&gt;正确的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正确的、手动的拷贝构造函数
B(const B&amp;amp; other)
    : A(other) // &amp;lt;-- 在初始化列表里，明确调用基类的拷贝构造函数！
{
    this-&amp;gt;b_val = other.b_val;
    std::cout &amp;lt;&amp;lt; &quot;B&apos;s CORRECT copy constructor called.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，&lt;code&gt;A(other)&lt;/code&gt; 这里的 &lt;code&gt;other&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;code&gt;A(const A&amp;amp;)&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;铁律&lt;/strong&gt;：自定义派生类拷贝构造函数时，&lt;strong&gt;永远不要忘记&lt;/strong&gt;在成员初始化列表中调用基类的拷贝构造函数：&lt;code&gt;Derived(const Derived&amp;amp; d) : Base(d) { ... }&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;赋值运算符&lt;/h4&gt;
&lt;p&gt;赋值运算符 &lt;code&gt;operator=&lt;/code&gt; 的行为与构造函数不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐式行为&lt;/strong&gt;：如果你不写派生类的 &lt;code&gt;operator=&lt;/code&gt;，编译器生成的版本会自动调用基类的 &lt;code&gt;operator=&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式行为&lt;/strong&gt;：如果你&lt;strong&gt;重载了&lt;/strong&gt;派生类的 &lt;code&gt;operator=&lt;/code&gt;，编译器&lt;strong&gt;不会&lt;/strong&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;pre&gt;&lt;code&gt;class B : public A {
public:
    B&amp;amp; operator=(const B&amp;amp; other) {
        if (this == &amp;amp;other) { // 1. 防止自我赋值
            return *this;
        }

        // 2. 手动调用基类的赋值运算符，把基类部分搞定
        // A::operator=(other); 两种写法都行
       *(A*)this = other; 

        // 3. 赋值派生类自己独有的成员
        // ...

        return *this; // 4. 返回*this以支持链式赋值
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 成员函数调用的动态绑定&lt;/h2&gt;
&lt;h3&gt;1. 静态绑定-&amp;gt;虚函数&lt;/h3&gt;
&lt;p&gt;当一个&lt;strong&gt;基类（父类）的指针或引用&lt;/strong&gt;，指向了一个&lt;strong&gt;派生类（子类）的对象&lt;/strong&gt;时，如果基类和派生类里都有一个同名函数（比如 &lt;code&gt;f()&lt;/code&gt;），那么通过这个指针调用函数，到底会执行谁的版本？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 基类指针 p，但它实际指向了一个派生类 B 的对象地址
B b;
A *p = &amp;amp;b; 

p-&amp;gt;f(); // ??? 这里调用的到底是 A::f() 还是 B::f()？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++ 默认采用的是&lt;strong&gt;静态绑定&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态绑定是什么？&lt;/strong&gt; 在&lt;strong&gt;编译&lt;/strong&gt;的时候，编译器就决定了要调用哪个函数。它做决定的依据是&lt;strong&gt;指针或引用自身的类型（静态类型）&lt;/strong&gt;，而不是它实际指向的对象的类型（动态类型）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对应到例子里：&lt;/strong&gt; 指针 &lt;code&gt;p&lt;/code&gt; 的类型被声明为 &lt;code&gt;A*&lt;/code&gt;。所以编译器看到 &lt;code&gt;p-&amp;gt;f()&lt;/code&gt;，就会说：“&lt;code&gt;p&lt;/code&gt; 是 &lt;code&gt;A&lt;/code&gt; 类型的指针，那就调用 &lt;code&gt;A&lt;/code&gt; 类的 &lt;code&gt;f()&lt;/code&gt; 函数吧。” &lt;strong&gt;它根本不关心 &lt;code&gt;p&lt;/code&gt; 在程序运行时到底指向了谁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论 (第2张图的重点)&lt;/strong&gt;： 默认情况下，&lt;code&gt;p-&amp;gt;f()&lt;/code&gt; 和 &lt;code&gt;x.f()&lt;/code&gt; 调用的&lt;strong&gt;都是 &lt;code&gt;A::f()&lt;/code&gt;&lt;/strong&gt;。这通常不是我们想要的结果。我们明明指向了一个 &lt;code&gt;B&lt;/code&gt; 对象，却没能调用 &lt;code&gt;B&lt;/code&gt; 的行为，这就失去了灵活性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决上面的问题，实现我们想要的“调用谁就执行谁的动作”这种智能行为，C++ 提供了&lt;strong&gt;虚函数&lt;/strong&gt;机制。&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;基类&lt;/strong&gt;中，使用 &lt;code&gt;virtual&lt;/code&gt; 关键字声明函数，就能启用&lt;strong&gt;动态绑定 (Dynamic Binding)&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;code&gt;A&lt;/code&gt; 的 &lt;code&gt;f()&lt;/code&gt; 前面加上 &lt;code&gt;virtual&lt;/code&gt; 关键字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    // 加上 virtual，告诉编译器这个函数要用“动态绑定”
    virtual void f(); 
};

class B : public A {
public:
    // 基类是 virtual 后，派生类中同名的函数自动也是 virtual
    void f(); 
};

B b;
A *p = &amp;amp;b;
p-&amp;gt;f(); // 现在，程序运行时会检查 p 指向的是 B 对象，于是调用 B::f()！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;virtual&lt;/code&gt; 关键字就像一个开关，它把函数调用的决策时机从&lt;strong&gt;编译时&lt;/strong&gt;推迟到了&lt;strong&gt;运行时&lt;/strong&gt;，从而实现了多态。&lt;/p&gt;
&lt;p&gt;如果要强制使用&lt;code&gt;A&lt;/code&gt;的&lt;code&gt;virtual f&lt;/code&gt;，也很简单：&lt;code&gt;p-&amp;gt;A::f()&lt;/code&gt;就好了。&lt;/p&gt;
&lt;h4&gt;不安全的析构问题&lt;/h4&gt;
&lt;p&gt;正常是这样写代码的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B* ptrB = new B();
delete ptrB;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，无论 &lt;code&gt;~A()&lt;/code&gt; 是不是虚函数，析构顺序永远是正确的 &lt;code&gt;~B() -&amp;gt; ~A()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是有的时候，我们会骗一下编译器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A* ptrA = new B(); // 指针是 A 类型，对象是 B 类型
delete ptrA;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，当编译器看到 &lt;code&gt;delete ptrA;&lt;/code&gt; 时，它遇到了一个决策难题。它只从表面上看到了 &lt;code&gt;ptrA&lt;/code&gt; 是一个 &lt;code&gt;A*&lt;/code&gt; 类型的指针。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;~A&lt;/code&gt;不是虚函数。编译器会采用&lt;strong&gt;静态绑定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它的“思考”过程是：“我手里的指针是 &lt;code&gt;A*&lt;/code&gt; 类型。析构函数 &lt;code&gt;~A()&lt;/code&gt; 不是 &lt;code&gt;virtual&lt;/code&gt; 的，这表示我不需要去探究这个指针背后到底藏着什么。我的任务就是调用 &lt;code&gt;A*&lt;/code&gt; 类型对应的析构函数。”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行结果&lt;/strong&gt;：程序&lt;strong&gt;只调用了 &lt;code&gt;A&lt;/code&gt; 的析构函数 &lt;code&gt;~A()&lt;/code&gt;&lt;/strong&gt;。它根本不知道这个指针实际指向的是一个 &lt;code&gt;B&lt;/code&gt; 对象，因此 &lt;code&gt;B&lt;/code&gt; 的析构函数 &lt;code&gt;~B()&lt;/code&gt; 被完全跳过了。产生内存泄漏！&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;~A&lt;/code&gt;是虚函数，这时候才会正确地调用出&lt;code&gt;~B&lt;/code&gt;，然后向上调用&lt;code&gt;~A&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;通过基类调用派生类独有函数&lt;/h4&gt;
&lt;p&gt;如果派生类 &lt;code&gt;B&lt;/code&gt; 有一个基类 &lt;code&gt;A&lt;/code&gt; 完全没有的函数，比如 &lt;code&gt;void g()&lt;/code&gt;，我们能通过基类指针 &lt;code&gt;p&lt;/code&gt; 调用它吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A { public: virtual void f(); };
class B : public A { public: void g(); };

A* p = new B;
p-&amp;gt;g(); // 编译错误！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么会编译错误？&lt;/strong&gt; 因为编译器进行静态类型检查时，发现 &lt;code&gt;p&lt;/code&gt; 是 &lt;code&gt;A*&lt;/code&gt; 类型，而 &lt;code&gt;class A&lt;/code&gt; 的定义里根本没有 &lt;code&gt;g()&lt;/code&gt; 这个函数。编译器直接就判错了，它根本不会去想 &lt;code&gt;p&lt;/code&gt; 将来可能会指向 &lt;code&gt;B&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;要想解决这个问题，我们必须把 &lt;code&gt;A*&lt;/code&gt; 类型的指针&lt;strong&gt;转换&lt;/strong&gt;成 &lt;code&gt;B*&lt;/code&gt; 类型的指针。书上介绍了两种方法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1）C风格的强制转换：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;((B*)p)-&amp;gt;g(); // 告诉编译器：“别管了，就当它是 B* 类型处理”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做非常危险。如果 &lt;code&gt;p&lt;/code&gt; 恰好指向的确实是 &lt;code&gt;B&lt;/code&gt; 对象，代码能正常工作。但如果 &lt;code&gt;p&lt;/code&gt; 指向的是一个 &lt;code&gt;A&lt;/code&gt; 对象呢？&lt;code&gt;A&lt;/code&gt; 对象内存里根本没有 &lt;code&gt;g()&lt;/code&gt; 函数，强制执行就会导致程序崩溃或出现无法预料的错误。这等于是在欺骗编译器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2）安全的C++动态类型转换 &lt;code&gt;dynamic_cast&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B* q = dynamic_cast&amp;lt;B*&amp;gt;(p); // 尝试将 p 转换为 B*
if (q != nullptr) {         // 如果转换成功...
    q-&amp;gt;g();                 // ...再安全地调用 g()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dynamic_cast&lt;/code&gt; 会在&lt;strong&gt;程序运行时&lt;/strong&gt;检查 &lt;code&gt;p&lt;/code&gt; 是否真的指向一个 &lt;code&gt;B&lt;/code&gt; 类型的对象（或者是 &lt;code&gt;B&lt;/code&gt; 的某个派生类）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果转换成功&lt;/strong&gt;，它会返回一个有效的 &lt;code&gt;B*&lt;/code&gt; 指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果转换失败&lt;/strong&gt;（比如 &lt;code&gt;p&lt;/code&gt; 实际指向的是一个 &lt;code&gt;A&lt;/code&gt; 对象），它会返回 &lt;code&gt;nullptr&lt;/code&gt; (空指针)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 纯虚函数、抽象类&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;纯虚函数 (Pure Virtual Function)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：它是一个只有声明、没有实现的虚函数。你通过在函数声明的末尾写上 &lt;code&gt;= 0;&lt;/code&gt; 来表明它是一个纯虚函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语法&lt;/strong&gt;：&lt;code&gt;virtual &amp;lt;返回类型&amp;gt; &amp;lt;函数名&amp;gt;(&amp;lt;参数列表&amp;gt;) = 0;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 它就像一个&lt;strong&gt;强制性的合同&lt;/strong&gt;。基类在这里规定：“任何继承我的非抽象子类，都&lt;strong&gt;必须&lt;/strong&gt;自己去实现这个函数。我自己不提供默认的实现，因为对我这个笼统的基类来说，谈论具体实现没有意义。”&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    virtual int f() = 0;
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;抽象类 (Abstract Class)&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;strong&gt;不能被实例化&lt;/strong&gt;。也就是说，你不能创建抽象类的对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：抽象类的存在不是为了创建它自己的对象，而是为了给它的所有派生类提供一个统一的&lt;strong&gt;接口（interface）**和**框架（framework）&lt;/strong&gt;。它定义了一系列所有“子孙后代”都必须遵守的规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Figure { // 抽象基类
public:
    virtual void draw() const = 0;     // 纯虚函数：每个图形都必须能被绘制
    virtual void input_data() = 0;   // 纯虚函数：每个图形都必须能输入自己的数据
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;课本上283页的代码中，&lt;code&gt;Rectangle&lt;/code&gt;, &lt;code&gt;Circle&lt;/code&gt;, &lt;code&gt;Line&lt;/code&gt; 这三个类都继承自 &lt;code&gt;Figure&lt;/code&gt;。它们通过提供 &lt;code&gt;draw()&lt;/code&gt; 和 &lt;code&gt;input_data()&lt;/code&gt; 函数的具体实现，履行了与基类签下的“合同”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Rectangle::draw()&lt;/code&gt; 会画一个矩形。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Circle::draw()&lt;/code&gt; 会画一个圆形。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Line::input_data()&lt;/code&gt; 会提示用户输入两个端点的坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，这三个类都是&lt;strong&gt;具体类 (Concrete Class)&lt;/strong&gt;，它们可以被实例化（创建对象）。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;FiguresMgr&lt;/code&gt; 管理器&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;FiguresMgr&lt;/code&gt; 类里有一个非常关键的成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Figure *figures[MAX_NUM_OF_FIGURES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个&lt;strong&gt;基类指针数组&lt;/strong&gt;。由于 &lt;code&gt;Rectangle&lt;/code&gt;, &lt;code&gt;Circle&lt;/code&gt;, &lt;code&gt;Line&lt;/code&gt; 对象都是 &lt;code&gt;Figure&lt;/code&gt; 的一种，所以这个数组可以存放指向这三种不同类型对象的指针。它并不知道，也不关心每个位置上存的到底是什么具体形状。&lt;/p&gt;
&lt;h2&gt;三. 多继承&lt;/h2&gt;
&lt;p&gt;如果我们想创建一个新类，它需要融合两个已有的、互不相关的类的功能，该怎么办？&lt;/p&gt;
&lt;p&gt;我们已经有两个设计好的类，&lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A&lt;/code&gt; 类有自己的成员：&lt;code&gt;int m;&lt;/code&gt; 和 &lt;code&gt;void fa(); B&lt;/code&gt; 类也有自己的成员：&lt;code&gt;int n;&lt;/code&gt; 和 &lt;code&gt;void fb();&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    int m;
    void fa() { /* ... */ };
};

class B {
public:
    int n;
    void fb() { /* ... */ };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们需要创建一个新类 &lt;code&gt;C&lt;/code&gt;，它要&lt;strong&gt;同时拥有 A 和 B 的所有成员&lt;/strong&gt;，并且还要有自己独有的成员 &lt;code&gt;int r;&lt;/code&gt; 和 &lt;code&gt;void fc();&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果我们只会单继承，可能会想出两种笨办法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法一：让 &lt;code&gt;C&lt;/code&gt; 继承 &lt;code&gt;A&lt;/code&gt;，然后手动把 &lt;code&gt;B&lt;/code&gt; 的代码抄一遍过来。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class C : public A { // C 继承 A
    int n;          // 从 B 手动复制过来
public:
    void fb() { /* ... */ }; // 从 B 手动复制过来
    int r;
    void fc() { /* ... */ };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方法二：让 &lt;code&gt;C&lt;/code&gt; 继承 &lt;code&gt;B&lt;/code&gt;，然后手动把 &lt;code&gt;A&lt;/code&gt; 的代码抄一遍过来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缺点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;概念上的混乱 (Conceptual Confusion)&lt;/strong&gt;：&lt;code&gt;A&lt;/code&gt; 类的 &lt;code&gt;m&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 类的 &lt;code&gt;n&lt;/code&gt; 本来是各自独立的属性，分属不同的类。现在你把它们强行塞进同一个 &lt;code&gt;C&lt;/code&gt; 类里，破坏了类的独立性和封装性，逻辑上不清晰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码维护的噩梦 (Maintenance Nightmare)&lt;/strong&gt;：这是最严重的问题。想象一下，一年后，你的同事在原始的 &lt;code&gt;A&lt;/code&gt; 类中修改了 &lt;code&gt;fa()&lt;/code&gt; 函数，修复了一个重要的 bug。但是，他根本不知道你曾经手动把 &lt;code&gt;fa()&lt;/code&gt; 的代码复制到了 &lt;code&gt;C&lt;/code&gt; 类里。&lt;strong&gt;结果就是，&lt;code&gt;A&lt;/code&gt; 类的 bug 修复了，但你的 &lt;code&gt;C&lt;/code&gt; 类里依然是那个陈旧的、有 bug 的版本！&lt;/strong&gt; 这会导致代码不一致，产生非常隐蔽的错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型系统的限制 (Type System Limitation)&lt;/strong&gt;：如果 &lt;code&gt;C&lt;/code&gt; 继承自 &lt;code&gt;A&lt;/code&gt;，那么在程序看来，一个 &lt;code&gt;C&lt;/code&gt; 对象“是”一个 &lt;code&gt;A&lt;/code&gt; 对象 (&lt;code&gt;C is an A&lt;/code&gt;)，但它“不是”一个 &lt;code&gt;B&lt;/code&gt; 对象。这意味着，如果有一个函数需要一个 &lt;code&gt;B&lt;/code&gt;类型的参数，你无法把 &lt;code&gt;C&lt;/code&gt; 对象传递给它。你的 &lt;code&gt;C&lt;/code&gt; 只能顶替 &lt;code&gt;A&lt;/code&gt; 或者 &lt;code&gt;B&lt;/code&gt; 中的一个，无法同时拥有两种身份。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 多继承派生类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// C 同时继承 A 和 B
class C : public A, public B {
public:
    int r;
    void fc() { /* ... */ };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语法非常直观：在类名后面用逗号隔开所有要继承的父类。这一行代码就完美地解决了第一页提出的所有问题。一个 &lt;code&gt;C&lt;/code&gt; 类的对象，现在既是 &lt;code&gt;A&lt;/code&gt; 类型，也是 &lt;code&gt;B&lt;/code&gt; 类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存布局&lt;/strong&gt;：一个 &lt;code&gt;C&lt;/code&gt; 类的对象在内存中，可以看成是&lt;strong&gt;一个 &lt;code&gt;A&lt;/code&gt; 子对象、一个 &lt;code&gt;B&lt;/code&gt; 子对象和 &lt;code&gt;C&lt;/code&gt; 自己成员的连续排列&lt;/strong&gt;。 这解释了为什么 &lt;code&gt;C&lt;/code&gt; 的对象能调用 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 的方法——因为它体内完整地包含了 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 的结构。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造函数的执行顺序&lt;/strong&gt;：创建 &lt;code&gt;c&lt;/code&gt; 对象时，构造函数的调用顺序是按照继承时声明的顺序：&lt;strong&gt;先调用 &lt;code&gt;A&lt;/code&gt; 的构造函数，再调用 &lt;code&gt;B&lt;/code&gt; 的构造函数，最后才调用 &lt;code&gt;C&lt;/code&gt; 自己的构造函数&lt;/strong&gt;。销毁时顺序相反。这很符合逻辑：必须先建好地基（父类），才能盖大楼（子类）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成员访问&lt;/strong&gt;：对 &lt;code&gt;c&lt;/code&gt; 对象来说，访问 &lt;code&gt;fa()&lt;/code&gt;、&lt;code&gt;fb()&lt;/code&gt;、&lt;code&gt;fc()&lt;/code&gt; 都没问题，因为它们都是 &lt;code&gt;C&lt;/code&gt; 的一部分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C c;
A* pa = &amp;amp;c; // pa 指向 c 对象中的 A 部分
B* pb = &amp;amp;c; // pb 指向 c 对象中的 B 部分
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;c&lt;/code&gt; 是整个 &lt;code&gt;C&lt;/code&gt; 对象的起始地址。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;A* pa = &amp;amp;c;&lt;/code&gt; 时，&lt;code&gt;pa&lt;/code&gt; 指针得到的地址，是 &lt;code&gt;c&lt;/code&gt; 对象内部那个 “A 子对象” 的起始地址。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;B* pb = &amp;amp;c;&lt;/code&gt; 时，&lt;code&gt;pb&lt;/code&gt; 指针得到的地址，是 &lt;code&gt;c&lt;/code&gt; 对象内部那个 “B 子对象” 的起始地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;pa&lt;/code&gt; 和 &lt;code&gt;pb&lt;/code&gt; 的地址值通常是不同的！&lt;/strong&gt; 因为在内存布局中，B 子对象跟在 A 子对象的后面。编译器会自动完成这个地址的调整。&lt;/p&gt;
&lt;h3&gt;2. 名冲突&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 中有一个同名的函数，比如都叫 &lt;code&gt;print()&lt;/code&gt;，那么 &lt;code&gt;c.print()&lt;/code&gt; 该调用谁的？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
public:
    void f(); // A 有一个 f()
    void g();
};

class B {
public:
    void f(); // B 也有一个 f()
    void h();
};

class C : public A, public B {
public:
    void func() {
        f(); // 错误！有歧义！
    }
};

// ...
C c;
c.f(); // 同样错误！有歧义！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你试图在 &lt;code&gt;C&lt;/code&gt; 中调用 &lt;code&gt;f()&lt;/code&gt; 时，编译器就懵了：“你到底想调用从 &lt;code&gt;A&lt;/code&gt; 继承来的 &lt;code&gt;f()&lt;/code&gt;，还是从 &lt;code&gt;B&lt;/code&gt; 继承来的 &lt;code&gt;f()&lt;/code&gt;？” 这种无法做出决定的情况，就叫做&lt;strong&gt;二义性 (ambiguity)&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;code&gt;::&lt;/code&gt;&lt;/strong&gt; 来“指名道姓”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class C : public A, public B {
public:
    void func() {
        A::f(); // OK，明确调用 A 的 f()
        B::f(); // OK，明确调用 B 的 f()
    }
};

// ...
C c;
c.A::f(); // OK，在外部调用 A 的 f()
c.B::f(); // OK，在外部调用 B 的 f()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 重复继承问题&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 本身都继承自同一个基类 &lt;code&gt;D&lt;/code&gt;，那么 &lt;code&gt;C&lt;/code&gt; 继承 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 后，体内是不是就有了两份来自 &lt;code&gt;D&lt;/code&gt; 的成员？这要如何解决？&lt;/p&gt;
&lt;p&gt;这是多继承里最著名的问题，也叫&lt;strong&gt;菱形继承问题 (Diamond Problem)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当多个父类（比如 &lt;code&gt;B&lt;/code&gt; 和 &lt;code&gt;C&lt;/code&gt;）继承自同一个基类（比如 &lt;code&gt;A&lt;/code&gt;），然后又有一个子类（比如 &lt;code&gt;D&lt;/code&gt;）同时继承了这两个父类（&lt;code&gt;B&lt;/code&gt; 和 &lt;code&gt;C&lt;/code&gt;）时，就会形成一个菱形的继承结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      A
     / \
    B   C
     \ /
      D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;A&lt;/code&gt; 类里有一个成员 &lt;code&gt;int x;&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;B&lt;/code&gt; 继承了 &lt;code&gt;A&lt;/code&gt;，所以 &lt;code&gt;B&lt;/code&gt; 有一份 &lt;code&gt;x&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt; 也继承了 &lt;code&gt;A&lt;/code&gt;，所以 &lt;code&gt;C&lt;/code&gt; 也有一份 &lt;code&gt;x&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;现在 &lt;code&gt;D&lt;/code&gt; 同时继承了 &lt;code&gt;B&lt;/code&gt; 和 &lt;code&gt;C&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个 &lt;code&gt;D&lt;/code&gt; 类的对象里，会包含两份 &lt;code&gt;A&lt;/code&gt; 的成员！&lt;/strong&gt; 一份是通过 &lt;code&gt;B&lt;/code&gt; 继承来的 (&lt;code&gt;B::x&lt;/code&gt;)，另一份是通过 &lt;code&gt;C&lt;/code&gt; 继承来的 (&lt;code&gt;C::x&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;这通常不是我们想要的。它不仅浪费内存，而且同样会造成&lt;strong&gt;二义性&lt;/strong&gt;：当你在 &lt;code&gt;D&lt;/code&gt; 的对象 &lt;code&gt;d&lt;/code&gt;上访问 &lt;code&gt;x&lt;/code&gt; 时 (&lt;code&gt;d.x&lt;/code&gt;)，编译器又会懵掉：“你要访问的是 &lt;code&gt;B&lt;/code&gt; 那条路上的 &lt;code&gt;x&lt;/code&gt;，还是 &lt;code&gt;C&lt;/code&gt; 那条路上的 &lt;code&gt;x&lt;/code&gt;？”&lt;/p&gt;
&lt;p&gt;为了解决这个问题，C++ 引入了&lt;strong&gt;虚基类&lt;/strong&gt;的概念。我们需要在“菱形”中间的那一层，也就是 &lt;code&gt;B&lt;/code&gt; 和 &lt;code&gt;C&lt;/code&gt; 继承 &lt;code&gt;A&lt;/code&gt; 的时候，使用 &lt;code&gt;virtual&lt;/code&gt; 关键字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A 是顶端基类
class A { public: int x; };

// B 和 C 在继承 A 时，使用 virtual 关键字
class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };

// D 的继承方式不变
class D : public B, public C {
public:
    void f() {
        x = 10; // OK！现在不再有歧义了
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;virtual&lt;/code&gt; 关键字的作用：&lt;/strong&gt; 它告诉编译器：“请确保 &lt;code&gt;A&lt;/code&gt; 这个基类在后续的任何多继承中，只保留&lt;strong&gt;一份&lt;/strong&gt;实例。” 这样做了之后，&lt;code&gt;D&lt;/code&gt; 类的对象在内存中就只有一个共享的 &lt;code&gt;A&lt;/code&gt; 子对象，因此也只有一份成员 &lt;code&gt;x&lt;/code&gt;。所有对 &lt;code&gt;x&lt;/code&gt; 的访问都指向这个唯一的实例，二义性就消除了。&lt;/p&gt;
&lt;h2&gt;四. 模板&lt;/h2&gt;
&lt;p&gt;我们能不能针对不同类型的数据实体，写出通用的工具函数，比如排序？更具体一些，能不能写一个排序函数，函数中仅仅实现算法，然后能同时支持&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;和自定义的对象？&lt;/p&gt;
&lt;p&gt;可以用函数重载，但这并不是解决这个问题的最佳方案，而且它违背了“代码复用”的初衷。我们对外，尽管能都使用&lt;code&gt;sort&lt;/code&gt;函数，但是对内，我们还是要同时写出三个版本的&lt;code&gt;sort&lt;/code&gt;，并且算法逻辑都要重新实现一遍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void sort(int elements[], unsigned int num) {
    cout &amp;lt;&amp;lt; &quot;调用了 int 版本的排序函数&quot; &amp;lt;&amp;lt; endl;
    for (int i = 0; i &amp;lt; num; ++i) {
        for (int j = i + 1; j &amp;lt; num; ++j) {
            // 这是排序的核心逻辑
            if (elements[i] &amp;gt; elements[j]) {
                int temp = elements[i];
                elements[i] = elements[j];
                elements[j] = temp;
            }
        }
    }
}

// 重载版本 2: 用于浮点数排序
void sort(double elements[], unsigned int num) {
    cout &amp;lt;&amp;lt; &quot;调用了 double 版本的排序函数&quot; &amp;lt;&amp;lt; endl;
    for (int i = 0; i &amp;lt; num; ++i) {
        for (int j = i + 1; j &amp;lt; num; ++j) {
            // 注意！这里的排序逻辑和上面完全一样！
            if (elements[i] &amp;gt; elements[j]) {
                double temp = elements[i];
                elements[i] = elements[j];
                elements[j] = temp;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码很冗余了。而且扩展性很差。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数重载的本质是为你提供一个统一的函数名，但允许它们有不同的参数列表和不同的实现逻辑。&lt;/strong&gt; 而在这里，我们的&lt;strong&gt;实现逻辑是相同的&lt;/strong&gt;，只是作用的数据类型不同。&lt;/p&gt;
&lt;p&gt;而且函数能重载，类可不能重载。类似的问题，我们能不能实现一个通用的、所有类型的都能使用的栈或者队列？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;程序设计中，一个程序实体能对多种类型的数据进行操作或描述的特性称为类属性（generics），具有类属性的程序实体通常有类属函数和类属类。类属函数是指一个函数能对不同类型的数据（参数）完成相同的操作；类属类是指一个类的成员类型可变但操作不变。&lt;/p&gt;
&lt;p&gt;基于具有类属性的程序实体进行程序设计的技术称为&lt;strong&gt;泛型程序设计（generic programming）&lt;/strong&gt;，它为软件复用提供了另一条途径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在C++中，泛型用模板实现。&lt;/p&gt;
&lt;h3&gt;1. 模板&lt;/h3&gt;
&lt;h4&gt;函数模板&lt;/h4&gt;
&lt;p&gt;在C++出现之前，C语言的程序员们用一种巧妙但复杂的方法来解决这个问题：&lt;strong&gt;&lt;code&gt;void\*&lt;/code&gt;&lt;/strong&gt; 通用指针。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;void*&lt;/code&gt; 是一种“无类型”的指针，它可以指向任何类型的数据。思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写一个通用的排序函数，它的参数不是 &lt;code&gt;int*&lt;/code&gt; 或 &lt;code&gt;double*&lt;/code&gt;，而是 &lt;code&gt;void*&lt;/code&gt;，这样它就能接收任何类型的数组地址。&lt;/li&gt;
&lt;li&gt;因为不知道数组元素的具体类型，所以也不知道每个元素占多大内存（&lt;code&gt;sizeof&lt;/code&gt;），也没法直接比较两个元素的大小（比如 &lt;code&gt;a &amp;gt; b&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;怎么办呢？把这两个信息也作为参数传给函数！
&lt;ul&gt;
&lt;li&gt;传入每个元素的&lt;strong&gt;大小&lt;/strong&gt; &lt;code&gt;element_size&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;传入一个&lt;strong&gt;比较函数&lt;/strong&gt;的地址（函数指针 &lt;code&gt;element_cmp&lt;/code&gt;），这个函数专门负责比较两个元素的大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，C语言版本的通用排序函数看起来非常复杂：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// C风格的通用排序
void sort(void *base, unsigned int num, unsigned int element_size, 
          bool (*element_cmp)(const void *, const void *));
&lt;/code&gt;&lt;/pre&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;li&gt;&lt;strong&gt;调用麻烦&lt;/strong&gt;：每次调用时，都需要提供一大堆参数，包括大小和比较函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++ 提供了一种更优雅、更安全的解决方案：&lt;strong&gt;模板 (Template)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你可以把模板看成是一个**“代码生成器”**。你写一个通用的函数模板，告诉编译器：“这是一个模板，请根据我调用它时使用的具体类型，自动帮我生成对应版本的函数。”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt; // 声明这是一个模板，T 是一个通用的类型参数
void sort(T elements[], unsigned int num) {
    // ... 函数体内部可以直接使用 T 类型的变量 ...
    // 比如直接用 &amp;lt; 或 &amp;gt; 比较 elements[i] 和 elements[j]
    if (elements[i] &amp;lt; elements[j]) {
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a[100];
double b[200];

sort(a, 100); // 1. 编译器看到你传入了 int 类型的数组 a
              // 2. 它会自动用 int 替换模板里所有的 T
              // 3. 然后生成一个专门给 int 用的 sort 函数，就像这样：
              //    void sort(int elements[], unsigned int num) { ... }

sort(b, 200); // 1. 编译器看到你传入了 double 类型的数组 b
              // 2. 它会自动用 double 替换模板里所有的 T
              // 3. 然后生成一个 double 版本的 sort 函数。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个过程叫做&lt;strong&gt;模板实例化 (Instantiation)&lt;/strong&gt; 和&lt;strong&gt;类型推导 (Type Deduction)&lt;/strong&gt;。编译器非常聪明，它会根据你传入的实参类型，自动推导出 &lt;code&gt;T&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;strong&gt;简洁&lt;/strong&gt;：代码量大大减少，逻辑清晰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：类型检查在&lt;strong&gt;编译时&lt;/strong&gt;就完成了。如果你的类型不支持 &lt;code&gt;&amp;lt;&lt;/code&gt; 比较，编译器会直接报错，而不是等到运行时才出问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高效&lt;/strong&gt;：它没有运行时的额外开销，因为最终执行的代码和手写的特定类型版本是一样的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果编译器无法判断是什么类型，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
T max(T a, T b) {
    retrun a &amp;gt; b ? a : b ;
}

int x,y;
double m, n;
... max(x, m);	// ? 类型是什么？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就需要显式类型转换，或者显式实例化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;... max((double)x, m); 		// 显式类型转换
... max&amp;lt;double&amp;gt; (x, m);		// 显式实例化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以传入非类型参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型参数 (Type Parameter)&lt;/strong&gt;：像 &lt;code&gt;class T&lt;/code&gt;，它是一个&lt;strong&gt;类型&lt;/strong&gt;的占位符。在实例化时，它会被换成一个具体的类型，比如 &lt;code&gt;int&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt; 或 &lt;code&gt;string&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;非类型参数 (Non-Type Parameter)&lt;/strong&gt;：像 &lt;code&gt;int size&lt;/code&gt;，它是一个&lt;strong&gt;常量值&lt;/strong&gt;的占位符。在实例化时，它会被换成一个具体的、在编译时就能确定的常量值，比如 &lt;code&gt;10&lt;/code&gt;、&lt;code&gt;256&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T, int size&amp;gt; // T 是类型参数，int size 是非类型参数
void f(T a) 
{
    T temp[size]; // 关键在这里！
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在C++中，当你定义一个像这样的静态数组时，数组的大小（&lt;code&gt;size&lt;/code&gt;）&lt;strong&gt;必须是一个在编译时就能确定的常量&lt;/strong&gt;。你不能使用一个普通的变量来定义它的大小。&lt;/p&gt;
&lt;p&gt;模板的作用是在&lt;strong&gt;编译期间&lt;/strong&gt;生成代码。当编译器准备为 &lt;code&gt;f&lt;/code&gt; 函数生成一个具体版本的代码时，它需要明确知道 &lt;code&gt;size&lt;/code&gt; 到底是多少，否则 &lt;code&gt;T temp[size];&lt;/code&gt; 这一行就无法通过编译。&lt;/p&gt;
&lt;p&gt;调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f&amp;lt;int, 10&amp;gt;(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这种带有非类型参数的模板，通常需要使用尖括号 &lt;code&gt;&amp;lt; &amp;gt;&lt;/code&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;pre&gt;&lt;code&gt;// 关键字 class 和 typename 在这里是等价的
template &amp;lt;class T1, class T2, ...&amp;gt; 
class 类名 {
    // ... 类成员声明 ...
    // 在这里面可以像使用普通类型一样使用 T1, T2
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
class Stack {
private:
    T buffer[100]; // 数据缓冲区，T 可以是 int, double 等任何类型
    int top;

public:
    Stack() { top = -1; }
    void push(const T &amp;amp;x); // 压栈的元素是 T 类型
    void pop(T &amp;amp;x);        // 出栈的元素也是 T 类型
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack&amp;lt;int&amp;gt; s1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器就会在后台生成一个 &lt;code&gt;int&lt;/code&gt; 版本的 &lt;code&gt;Stack&lt;/code&gt; 类，其中所有的 &lt;code&gt;T&lt;/code&gt; 都被替换成了 &lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack&amp;lt;double&amp;gt; s2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器则会生成一个 &lt;code&gt;double&lt;/code&gt; 版本的 &lt;code&gt;Stack&lt;/code&gt; 类。&lt;code&gt;s1&lt;/code&gt; 和 &lt;code&gt;s2&lt;/code&gt; 是两种完全不同类型的对象。&lt;/p&gt;
&lt;p&gt;如果成员函数在类外定义，语法会稍微复杂一点，必须同时包含模板声明和类的模板参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt; // 1. 模板声明不能少
void Stack&amp;lt;T&amp;gt;::push(const T &amp;amp;x) { // 2. 类名后面要带 &amp;lt;T&amp;gt;
    // ... 实现 ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模板的复用&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;模板可以实现一种特殊的多态，称为参数化多态。模板是一段带有类型参数的代码，给该参数提供不同的类型就能得到多个不同的代码，即一段代码有多种解释。使用一个模板之前首先要对其进行实例化（用一个具体的类型去替代模板的类型参数），而实例化是在编译时进行的，一定要见到相应的源代码，否则无法进行实例化！因此，模板属于源代码复用。&lt;/p&gt;
&lt;p&gt;函数模板的实例化可以是隐式的，也可以是显式的；而类模板的实例化则是显式进行的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;课本上这一块内容，主要意思就是：&lt;/p&gt;
&lt;p&gt;模板是编译时就直接完成对应类型实现的代码实现的，如果文件中没有真正实例化，对应类型的代码不会被实现。C++编译器是先单独编译每个&lt;code&gt;.cpp&lt;/code&gt;文件，分别生成&lt;code&gt;.obj&lt;/code&gt;文件，最后再链接起来的。因此如果单个文件中，调用一个不是在本文件中定义的模板，会产生错误，因为模板所处的文件编译时，编译器根本不知道有这么个实例化，也就没生成对应类型实现的代码。&lt;/p&gt;
&lt;p&gt;实现不完整，也会发生问题。&lt;/p&gt;
&lt;p&gt;一个按照习惯会出错的地方就是这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// S.h
template &amp;lt;class T&amp;gt;
class T {
public:
    void f();
};

// f.cpp
#include &quot;S.h&quot;
template &amp;lt;class T&amp;gt;
void S&amp;lt;T&amp;gt;::f() {
    // 具体实现...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们习惯把具体函数的实现放在&lt;code&gt;.cpp&lt;/code&gt;文件中，然后在&lt;code&gt;main.cpp&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.cpp
#include &quot;S.h&quot;
int main(){
    S&amp;lt;float&amp;gt; s1;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这能通过编译吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译器处理 &lt;code&gt;main.cpp&lt;/code&gt; 时：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它通过 &lt;code&gt;#include &quot;s.h&quot;&lt;/code&gt; 看到了 &lt;code&gt;S&lt;/code&gt; 这个模板的&lt;strong&gt;声明&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当它看到 &lt;code&gt;S&amp;lt;int&amp;gt; s2;&lt;/code&gt; 这行代码时，它知道 &lt;code&gt;S&amp;lt;int&amp;gt;&lt;/code&gt; 是一个合法的类型，也知道它应该有一个 &lt;code&gt;f()&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;但是，它没有 &lt;code&gt;f()&lt;/code&gt; 的&lt;strong&gt;具体实现&lt;/strong&gt;（因为实现在 &lt;code&gt;s.cpp&lt;/code&gt; 里）。这时，编译器不会报错。它会想：“好的，我现在没有实现代码，但我相信别的某个 &lt;code&gt;.cpp&lt;/code&gt; 文件会有，我就先在这里留个‘欠条’，等最后的&lt;strong&gt;链接&lt;/strong&gt;阶段让链接器去把它们拼起来吧。”&lt;/li&gt;
&lt;li&gt;所以，&lt;code&gt;main.cpp&lt;/code&gt; 顺利地编译成了一个 &lt;code&gt;main.obj&lt;/code&gt; 文件，这个文件里包含了对 &lt;code&gt;S&amp;lt;int&amp;gt;::f()&lt;/code&gt; 的一个引用（那张“欠条”）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;编译器处理 &lt;code&gt;s.cpp&lt;/code&gt; 时：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它看到了 &lt;code&gt;S&amp;lt;T&amp;gt;::f()&lt;/code&gt; 的具体实现。&lt;/li&gt;
&lt;li&gt;但是，在这个文件里，&lt;strong&gt;没有任何地方用到了 &lt;code&gt;S&amp;lt;int&amp;gt;&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;由于模板的“按需实例化”原则，编译器觉得既然没人用 &lt;code&gt;S&amp;lt;int&amp;gt;&lt;/code&gt;，那它就根本&lt;strong&gt;不会去生成 &lt;code&gt;S&amp;lt;int&amp;gt;::f()&lt;/code&gt; 的具体机器码&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;所以，&lt;code&gt;s.cpp&lt;/code&gt; 也顺利地编译成了一个 &lt;code&gt;s.obj&lt;/code&gt; 文件，但这个文件里&lt;strong&gt;不包含&lt;/strong&gt;我们需要的 &lt;code&gt;S&amp;lt;int&amp;gt;::f()&lt;/code&gt; 的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;链接器 (Linker) 工作时：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链接器把 &lt;code&gt;main.obj&lt;/code&gt; 和 &lt;code&gt;s.obj&lt;/code&gt; 等所有 &lt;code&gt;.obj&lt;/code&gt; 文件拿过来，准备把它们“粘”成一个可执行文件。&lt;/li&gt;
&lt;li&gt;它看到 &lt;code&gt;main.obj&lt;/code&gt; 里的那张“欠条”，说：“我需要 &lt;code&gt;S&amp;lt;int&amp;gt;::f()&lt;/code&gt; 的代码，谁有？”&lt;/li&gt;
&lt;li&gt;它翻遍了 &lt;code&gt;s.obj&lt;/code&gt; 以及所有其他 &lt;code&gt;.obj&lt;/code&gt; 文件，发现谁也没有提供这份代码。&lt;/li&gt;
&lt;li&gt;这时，链接器就只能报错了：“&lt;strong&gt;未定义的引用 (Undefined Reference)&lt;/strong&gt;”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决方案就是把模板的定义和实现都放在一个&lt;code&gt;.h&lt;/code&gt;文件中就行了。&lt;/p&gt;
&lt;h3&gt;2. 基于STL的编程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;STL (Standard Template Library)&lt;/strong&gt;，也就是C++的&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;ol&gt;
&lt;li&gt;&lt;strong&gt;序列容器 (Sequence Containers)&lt;/strong&gt;：元素按顺序排列。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt;：&lt;strong&gt;动态数组&lt;/strong&gt;。支持快速随机访问（像数组一样用 &lt;code&gt;[]&lt;/code&gt;），在尾部增删元素很快。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt;：&lt;strong&gt;双向链表&lt;/strong&gt;。支持在任意位置快速插入和删除，但随机访问很慢。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deque&lt;/code&gt;：&lt;strong&gt;双端队列&lt;/strong&gt;。是 &lt;code&gt;vector&lt;/code&gt; 和 &lt;code&gt;list&lt;/code&gt; 的折中，支持在头部和尾部快速增删。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关联容器 (Associative Containers)&lt;/strong&gt;：元素自动排序。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt; / &lt;code&gt;multiset&lt;/code&gt;：&lt;strong&gt;集合&lt;/strong&gt;。内部是红黑树，元素唯一且有序。&lt;code&gt;multiset&lt;/code&gt; 允许元素重复。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; / &lt;code&gt;multimap&lt;/code&gt;：&lt;strong&gt;映射&lt;/strong&gt;。存储键-值对（key-value），根据 key 排序。&lt;code&gt;map&lt;/code&gt; 的 key 唯一，&lt;code&gt;multimap&lt;/code&gt; 允许 key 重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器适配器 (Container Adapters)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stack&lt;/code&gt;：&lt;strong&gt;栈&lt;/strong&gt;。后进先出 (LIFO)，通常基于 &lt;code&gt;deque&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue&lt;/code&gt;：&lt;strong&gt;队列&lt;/strong&gt;。先进先出 (FIFO)，通常基于 &lt;code&gt;deque&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority_queue&lt;/code&gt;：&lt;strong&gt;优先队列&lt;/strong&gt;。最大（或最小）的元素总是在队首，通常基于 &lt;code&gt;vector&lt;/code&gt; 和堆 (heap) 实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;stack&lt;/code&gt;, &lt;code&gt;queue&lt;/code&gt;, &lt;code&gt;priority_queue&lt;/code&gt; 是“适配器”，它们限制了原有容器的功能，&lt;strong&gt;不支持迭代器&lt;/strong&gt;，因此你不能把它们直接用在 &lt;code&gt;sort&lt;/code&gt;、&lt;code&gt;find&lt;/code&gt; 等通用算法上。&lt;/p&gt;
&lt;p&gt;如果你想在容器里存储自己写的类对象，比如 &lt;code&gt;vector&amp;lt;Student&amp;gt;&lt;/code&gt;，那么你的 &lt;code&gt;Student&lt;/code&gt; 类可能需要满足一些条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于所有容器&lt;/strong&gt;：你的类必须是&lt;strong&gt;可复制&lt;/strong&gt;和&lt;strong&gt;可赋值&lt;/strong&gt;的，因为容器在内部可能需要拷贝或移动元素。这意味着你需要一个正常的&lt;strong&gt;拷贝构造函数&lt;/strong&gt;和&lt;strong&gt;赋值运算符 (&lt;code&gt;operator=&lt;/code&gt;)&lt;/strong&gt;。（通常编译器会为你生成默认的，但涉及指针时需小心深浅拷贝）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于关联容器（如 &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;）&lt;/strong&gt;：因为它们需要对元素排序，所以你的类必须是&lt;strong&gt;可比较&lt;/strong&gt;的。默认情况下，你需要重载&lt;strong&gt;小于号运算符 (&lt;code&gt;operator&amp;lt;&lt;/code&gt;)&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;迭代器&lt;/h4&gt;
&lt;p&gt;迭代器是连接容器和算法的“手”或“胶水”。它的设计思想就是模仿C++中的指针，但功能更通用、&lt;/p&gt;
&lt;p&gt;一个指针最基本的两项操作是什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解引用（Dereferencing）：通过 &lt;code&gt;*&lt;/code&gt; 获取指针指向的内存里的值。&lt;/li&gt;
&lt;li&gt;移动（Incrementing/Decrementing）：通过 &lt;code&gt;++&lt;/code&gt; 或 &lt;code&gt;--&lt;/code&gt; 移动指针，让它指向下一个或上一个内存地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;迭代器就封装了这些核心功能，并根据其能力的大小，分成了五个不同的类别。&lt;/p&gt;
&lt;p&gt;并非所有容器都能支持所有类型的移动。比如，链表就不能像数组那样随机跳到任意一个元素，它只能一个一个地移动。为了精确描述不同容器提供的遍历能力，STL定义了五种迭代器，它们的能力是层层递进的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别 (Category)&lt;/th&gt;
&lt;th&gt;主要能力&lt;/th&gt;
&lt;th&gt;能做什么&lt;/th&gt;
&lt;th&gt;通俗比喻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;输出 (Output)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*it = value; ++it;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;只写&lt;/strong&gt;，向前移动，一次性&lt;/td&gt;
&lt;td&gt;打印机，只能顺着往下打印，不能回头读。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;输入 (Input)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value = *it; ++it;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;只读&lt;/strong&gt;，向前移动，一次性&lt;/td&gt;
&lt;td&gt;键盘输入流，读过一个字符就没了，不能回头重读。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;前向 (Forward)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value = *it; *it = value; ++it;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;可读可写&lt;/strong&gt;，向前移动，&lt;strong&gt;可多次读写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一本只能往前翻的书，你可以反复读当前页。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;双向 (Bidirectional)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;前向所有功能, &lt;code&gt;--it;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可读可写，&lt;strong&gt;可双向移动&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一本可以前后随意翻页的书。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;随机访问 (Random-Access)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;双向所有功能, &lt;code&gt;it + n; it - n; it[n];&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可读可写，双向移动，&lt;strong&gt;可随机跳跃&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一本有详细目录和页码的书，可以瞬间翻到任何一页。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这个关系就像一个能力升级树&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250617104120117.png&quot; alt=&quot;image-20250617104120117&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>面向对象考前速通笔记 (4) 文件I/O、异常处理</title><link>https://fuwari.vercel.app/posts/course/ooap/oopa-note-4/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/ooap/oopa-note-4/</guid><pubDate>Mon, 16 Jun 2025 09:13:04 GMT</pubDate><content:encoded>&lt;p&gt;C++中，输入输出是基于字节流（stream）的操作。在进行操作的时候，可以把输入的数据堪称逐个字节地从外设流入到计算机内部。输出则是逐个字节流出到外设。&lt;/p&gt;
&lt;p&gt;C++中有C语言保留下来的输入输出函数，比如&lt;code&gt;scanf&lt;/code&gt;和&lt;code&gt;printf&lt;/code&gt;，这是过程式的。&lt;/p&gt;
&lt;p&gt;还有C++ I/O库中的类和对象，可以实现面向对象的输入/输出，例如，I/O库中的对象&lt;code&gt;cin&lt;/code&gt;可以实现键盘的输入，&lt;code&gt;cout&lt;/code&gt;可以用于显示字符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cout&lt;/code&gt; 就是一个&lt;strong&gt;对象&lt;/strong&gt;。它是 C++ 标准库根据 &lt;code&gt;ostream&lt;/code&gt; 这个&lt;strong&gt;类&lt;/strong&gt;提前为我们创建好的一个具体实例。它的任务就是把数据输出到控制台（显示器）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cin&lt;/code&gt; 也是一个&lt;strong&gt;对象&lt;/strong&gt;。它是根据 &lt;code&gt;istream&lt;/code&gt; (输入流) 这个&lt;strong&gt;类&lt;/strong&gt;创建的实例，专门负责从键盘读取数据。&lt;/p&gt;
&lt;p&gt;我们使用 &lt;code&gt;cout&lt;/code&gt; 时，就是在操作一个具体的“输出流对象”，命令它把我们的数据显示出来。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int age = 20;
cout &amp;lt;&amp;lt; &quot;My age is: &quot; &amp;lt;&amp;lt; age; // 我们命令 cout 对象，把字符串和变量 age 的值显示出来。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一. 面向控制台的IO&lt;/h2&gt;
&lt;h3&gt;1. 控制台输入&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cin&lt;/code&gt; 也是一个&lt;strong&gt;对象&lt;/strong&gt;。它是根据 &lt;code&gt;istream&lt;/code&gt; (输入流) 这个&lt;strong&gt;类&lt;/strong&gt;创建的实例。同理&lt;code&gt;cout&lt;/code&gt; 是 &lt;code&gt;ostream&lt;/code&gt; 类的对象。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250617112713193.png&quot; alt=&quot;image-20250617112713193&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这张图非常重要，它告诉我们：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ios&lt;/code&gt; 是这个家族的“老祖宗”，它定义了一些所有 I/O 流都共有的基础属性和功能（比如管理流的状态）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;istream&lt;/code&gt; (输入流) 和 &lt;code&gt;ostream&lt;/code&gt; (输出流) 都继承自 ios。&lt;code&gt;istream&lt;/code&gt; 专注于输入，定义了 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 操作符。&lt;code&gt;ostream&lt;/code&gt; 专注于输出，定义了 &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 操作符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iostream&lt;/code&gt; 同时继承了 &lt;code&gt;istream&lt;/code&gt; 和 &lt;code&gt;ostream&lt;/code&gt;，所以它既能输入也能输出。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ifstream&lt;/code&gt;, &lt;code&gt;ofstream&lt;/code&gt;, &lt;code&gt;fstream&lt;/code&gt; 等是用来操作文件的，原理类似，我们以后会学到。&lt;/p&gt;
&lt;p&gt;在使用I/O库的时候，首先要创建I/O对象，然后调用这个对象进行基于字节流代码输入/输出操作。这些对象有些有重载了 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 运算符，可以用于基本数据类型的输入和输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;istream in(...); // 手动创建一个输入对象
in &amp;gt;&amp;gt; my_variable;	// 输入给变量

ostream out(...); // 手动创建一个输出对象
out &amp;lt;&amp;lt; &quot;Hello&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们好像很少这样子写，我们写的总是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cin &amp;gt;&amp;gt; my_variable;
cout &amp;lt;&amp;lt; &quot;Hello&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有遇到过 &lt;code&gt;istream in(...)&lt;/code&gt; 这种写法，是因为 C++ 标准库已经&lt;strong&gt;提前帮你把这件事做好了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;针对控制台的输入/输出，在I/O库中预先定义了四个I/O对象&lt;code&gt;cin&lt;/code&gt;,&lt;code&gt;cout&lt;/code&gt;,&lt;code&gt;cerr&lt;/code&gt;以及&lt;code&gt;clog&lt;/code&gt;。这些可以直接用。这四个对象都是在头文件&lt;code&gt;iostream&lt;/code&gt;中定义的。所以要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 控制台输出&lt;/h3&gt;
&lt;p&gt;和输入类似的，不过要注意，输出指向字符的指针的时候，不是输出指针的值，而是自动转换成了它所指向的字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const char *p = &quot;Hello world!&quot;;

cout &amp;lt;&amp;lt; p;		// 输出字符串 Hello world!
cout &amp;lt;&amp;lt; (void *)p;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;输出控制符&lt;/h4&gt;
&lt;p&gt;我们有的时候会用到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;iomanip&amp;gt;

cout &amp;lt;&amp;lt; &quot;abc&quot; &amp;lt;&amp;lt; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;endl&lt;/code&gt;是回车，这实际上就是操纵符。&lt;/p&gt;
&lt;p&gt;可以用&lt;code&gt;hex&lt;/code&gt;操纵符把后续的输出格式改成十六进制。（是后续，也就是说不改回来这个程序里就一直这样了）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;iomanip&amp;gt;
using namespace std;

int main() {
	int x = 10;
	cout &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; x &amp;lt;&amp;lt; x &amp;lt;&amp;lt; oct &amp;lt;&amp;lt; x &amp;lt;&amp;lt; endl;
	cout &amp;lt;&amp;lt; x ;
	return 0; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250617114540470.png&quot; alt=&quot;image-20250617114540470&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到不手动切回来，后续就一直这样子了。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250617114653357.png&quot; alt=&quot;image-20250617114653357&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;3. 重载&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;,&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;如果有个类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    int x, y;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要直接输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A {
    int x, y;
    
    friend ostream&amp;amp; operator &amp;lt;&amp;lt; (ostream&amp;amp; out, const A &amp;amp;a);	// 访问私有对象要声明友元
};

ostream&amp;amp; operator &amp;lt;&amp;lt; (ostream&amp;amp; out, const A &amp;amp;a) {
    out &amp;lt;&amp;lt; a.x &amp;lt;&amp;lt; a.y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 面向文件的IO&lt;/h2&gt;
&lt;p&gt;（下面的都略了，看书就好了，不过这里都会考）&lt;/p&gt;
&lt;h2&gt;三. 面向字符串的IO&lt;/h2&gt;
&lt;h2&gt;四. C++ 异常处理&lt;/h2&gt;
</content:encoded></item><item><title>2022年蓝桥杯Python A组 省赛 做题记录</title><link>https://fuwari.vercel.app/posts/acm/lanqqiao-pa-2022-0/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/lanqqiao-pa-2022-0/</guid><pubDate>Sat, 01 Mar 2025 11:28:14 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;做题进度，目前还剩：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] H（30pts，还没de出bug）&lt;/li&gt;
&lt;li&gt;[ ] J（还没做）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他均已完成。&lt;/p&gt;
&lt;h2&gt;A. 裁纸刀&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2060/learning&lt;/p&gt;
&lt;p&gt;经过观察可以发现不管怎么切，刀数都是一样的。因此直接给出答案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def solve(n, m):
    return 4 + n * m - 1

n, m = 20, 22
print(solve(n, m))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B. 寻找整数&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2131/learning&lt;/p&gt;
&lt;p&gt;也就是说给了一堆模的方程，求这个数最小值。&lt;/p&gt;
&lt;p&gt;最朴素的暴力跑 $10^{17}$（$2^{60}$ 左右） 三个小时肯定跑不完。最快的方法应该是逐步合并的CRT，但是这题通过简单优化暴力，也可以很快跑出来。&lt;/p&gt;
&lt;p&gt;暴力的思路是，假设我们目前已经知道了一个同余方程
$$
n \equiv A (\mod M)
$$
那么现在 $n$ 最小肯定是 $A$。&lt;/p&gt;
&lt;p&gt;现在新加入一个同余方程，要求 $n$ 也满足
$$
n\equiv r (\mod m)
$$
假设这个是一定能被满足的（题意，所以无需判断无解的情况），新的模数 $M&apos;$ 很显然是 $\text{lcm} (M,m)$。那我们暴力以 $M$ 的步长，寻找下一个最小的同时满足前面两个方程的 $n&apos;$，因为是最小的，也就是新的 $A&apos;$。这样我们就得到了新的
$$
n\equiv A&apos; (\mod M&apos;)
$$
然后这样跳个50次就得到了答案。不会分析这个时间复杂度，但是很显然就是能很快跑出来的，毕竟这个$M$ 是一直在做lcm，指数级别增大的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import math

def solve(data):
    n, step = data[0]
    for i in range(1, len(data)):
        m, r = data[i]
        while n % m != r:
            n += step
        step = (step * m) // math.gcd(step, m)
    return n

if __name__ == &apos;__main__&apos;:
    data = [
        (2, 1), (3, 2), (4, 1), (5, 4), (6, 5), (7, 4), (8, 1), (9, 2),
        (10, 9), (11, 0), (12, 5), (13, 10), (14, 11), (15, 14), (16, 9),
        (17, 0), (18, 11), (19, 18), (20, 9), (21, 11), (22, 11), (23, 15),
        (24, 17), (25, 9), (26, 23), (27, 20), (28, 25), (29, 16), (30, 29),
        (31, 27), (32, 25), (33, 11), (34, 17), (35, 4), (36, 29), (37, 22),
        (38, 37), (39, 23), (40, 9), (41, 1), (42, 11), (43, 11), (44, 33),
        (45, 29), (46, 15), (47, 5), (48, 41), (49, 46)
    ]
    
    ans = solve(data)
    print(ans)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C. 质因数个数&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2155/learning&lt;/p&gt;
&lt;p&gt;最大运行时间 10s，$n$ 最大 1e16，$O(\sqrt n)$ 的算法应该可以过。&lt;/p&gt;
&lt;p&gt;一开始想手写一个筛法，筛 $\sqrt n$ 以内的所有质数即可。但是马上MLE了，1e8的&lt;code&gt;set&lt;/code&gt;是存不下的应该，虽然算了下512MB可以存的下。然后直接分解质因数，对于质因数直接把全部因子都除掉，这样可以保证下一个能取模的数一定还是质数（更小的因子已经没了）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 90 %
import os
import sys

n = int(input())

ans = 0

x = 2
while x * x &amp;lt;= n:
    if n % x == 0:
        while n % x == 0:
            n = n // x
        ans += 1
    x += 1

if n != 1:
    ans += 1
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写只有90分，还是T了一个点。需要继续优化，进一步的优化方案逐步如下。&lt;/p&gt;
&lt;p&gt;① 必须注意Python中&lt;code&gt;for .. range&lt;/code&gt;比&lt;code&gt;while&lt;/code&gt;要快得多，因为&lt;code&gt;for ..  range&lt;/code&gt;遍历底层是由C语言实现的。&lt;/p&gt;
&lt;p&gt;这样优化后，本地测试，运行速度从 &lt;code&gt;9s&lt;/code&gt; 可以优化到 &lt;code&gt;7s&lt;/code&gt;，那很好了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys

n = int(input())

ans = 0

import math
lim = math.floor(math.sqrt(n)) + 1
for x in range(2, lim):
    if x * x &amp;gt; n:
        break
    if n % x == 0:
        while n % x == 0:
            n = n // x
        ans += 1

if n != 1:
    ans += 1
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是蓝桥杯的古董机好像还不够，还是会T一个点。&lt;/p&gt;
&lt;p&gt;② 发现是因为引入&lt;code&gt;math&lt;/code&gt;库好像会慢一点点，去掉&lt;code&gt;math&lt;/code&gt;库就好了。现在可以A了。但是看时间有点极限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 100 %

import os
import sys
import math

n = int(input())

ans = 0

for x in range(2, int(n**0.5)+1):
# lim = math.floor(math.sqrt(n)) + 1
# for x in range(2, lim):

    if x * x &amp;gt; n:
        break
    if n % x == 0:
        while n % x == 0:
            n = n // x
        ans += 1

if n != 1:
    ans += 1
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 继续优化的话，就是对2特判，这样后面就只需要枚举奇数即可。下面是最后时间稳过的代码，本地运行从前面的&lt;code&gt;9s&lt;/code&gt;、&lt;code&gt;7s&lt;/code&gt;到&lt;code&gt;3.5 s&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys
import math

n = int(input())

ans = 0

if n % 2 == 0:
    while n % 2 == 0:
        n = n // 2
    ans += 1

for x in range(3, int(n**0.5)+1, 2):
    if x * x &amp;gt; n:
        break
    if n % x == 0:
        while n % x == 0:
            n = n // x
        ans += 1

if n != 1:
    ans += 1
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;D. 矩形拼接&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2154/learning&lt;/p&gt;
&lt;p&gt;很容易想到是那种史山模拟题，有一大堆if else。只能试图比较有条理的枚举。&lt;/p&gt;
&lt;p&gt;我的思路是，首先任意选两块拼一起，最坏情况就是随便乱放的时候，会有8条边。&lt;/p&gt;
&lt;p&gt;一种一定能达到的更优情况，是两条边共线的情况：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250213102124921.png&quot; alt=&quot;image-20250213102124921&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这种情况可以视为一个大矩形有一个缺口。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20250213102420191.png&quot; alt=&quot;image-20250213102420191&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;不妨在假设第三个矩形一定填在这个叠起来的（最多6边）形的某一个边上。这样就能直接枚举了。（我三个矩形枚举所有可能排列）。&lt;/p&gt;
&lt;p&gt;这样，每个询问，搜索的次数是 $3!\times 2^3 \times 6=36\times 8$，最后的时间复杂度是可以接受的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    from debug_utils import debug
except:
    def debug(*args):
        pass

import os
import sys

def solve():
    a1, b1, a2, b2, a3, b3 = map(int, input().split())
    rects = [[a1, b1], [a2, b2], [a3, b3]]
    ans = 8
    for base in rects:
        for s in range(2):
            down, near = base[s], base[1-s]
            for above in rects:
                if base is above:
                    continue
                for t in range(2):
                    up, left = above[t], above[1-t]
                    if up &amp;gt; down:
                        continue
                    &quot;&quot;&quot;
                               up
                             |----|
                    left     |    |
                             |---------|
                    near     |         |
                             ----------|
                                down
                    &quot;&quot;&quot;
                    if down != up:
                        angles = [down - up, left, up, left + near, down, near]
                    else:
                        angles = [up, left + near, down, left + near]
                    for rect in rects:
                        if rect is not base and rect is not above:
                            for u in range(2):
                                a, b = rect[u], rect[1-u]
                                &quot;&quot;&quot;
                                    a
                                    |------|
                                b   |      |
                                    --------
                                &quot;&quot;&quot;
                                normal = len(angles) + 2
                                now = normal
                                if len(angles) == 6:    # 存在270度角
                                    now = min(now, normal - ((2 if a == angles[0] else 0) + (2 if b == angles[1] else 0)) )
                                # 普通处理90度
                                for idx in range(len(angles)):
                                    if len(angles) == 6 and idx &amp;lt;= 1:
                                        continue
                                    c = angles[idx]
                                    now = min(now, normal - (2 if c == a else 0))
                                ans = min(now, ans)
    print(ans)


t = int(input())
for _ in range(t):
    solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;E. 消除游戏&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2127/learning&lt;/p&gt;
&lt;p&gt;纯模拟题，感觉最简单的写法就是直接 $O(n)$ 模拟一个链表，初始遍历一遍找到可以删的节点，然后每次删除时把这个节点左边、右边、左边的左边、右边的右边，4个节点都纳入考虑范围。（然后就和下面的代码一样模拟成构思了，写了快一百多行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys

class Node:
    __slots__ = (&apos;val&apos;, &apos;prev&apos;, &apos;next&apos;, &apos;alive&apos;)
    def __init__(self, val, prev = None, next = None):
        self.val = val
        self.prev = prev
        self.next = next
        self.alive = True
    def __repr__(self):
        return str(self.val)

class Linklist:
    def __init__(self):
        self.head = Node(None)  # 哨兵头尾
        self.tail = Node(None)
        self.head.next = self.tail
        self.tail.prev = self.head
    def insert(self, val, node = None):
        if node == None:
            node = self.tail.prev
        one = Node(val, node, node.next)
        node.next = one
        one.next.prev = one
        return one
    def remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev
        node.alive = False
        
    def __repr__(self):
        line = []
        now = self.head.next
        while now is not self.tail:
            line.append(now.val)
            now = now.next
        ret = &apos;&apos;.join(line)
        return ret if ret != &apos;&apos; else &apos;EMPTY&apos;


def solve():
    lines = sys.stdin.read().splitlines()

    s = lines[0]
    if s == &apos;&apos;:
        print(&quot;EMPTY&quot;)
        return 

    n = len(s)

    lst = Linklist()
    que = []

    for i in range(n):
        ptr = lst.insert(s[i])
        if  (0 &amp;lt; i &amp;lt; n-1 and ( (s[i-1] == s[i] != s[i+1]) or (s[i-1] != s[i] == s[i+1]) ) ) or \
            (0 &amp;lt;= i and i+2 &amp;lt; n and s[i] != s[i+1] == s[i+2]) or \
            (i - 2 &amp;gt;= 0 and i &amp;lt; n and s[i-2] == s[i-1] != s[i]):
            que.append(ptr)
            ptr.alive = False
    
    while len(que) != 0:
        wait = []
        for p in que:
            lst.remove(p)
            if p.prev.val is not None and p.prev.alive:
                wait.append(p.prev)
            if p.prev.prev is not None and p.prev.prev.val is not None and p.prev.prev.alive:
                wait.append(p.prev.prev)
            if p.next.val is not None and p.next.alive:
                wait.append(p.next)
            if p.next.next is not None and p.next.next.val is not None and p.next.next.alive:
                wait.append(p.next.next)
        new_que = []
        for p in wait:
            if p.alive == True and \
                ((((p.prev.val == p.val != p.next.val and p.next.val is not None) or (p.prev.val != p.val == p.next.val and p.prev.val is not None)) ) or \
            (p.next.next is not None and p.val != p.next.val == p.next.next.val) or \
            (p.prev.prev is not None and p.prev.prev.val == p.prev.val != p.val)):
                new_que.append(p)
                p.alive = False
        que = new_que
    print(lst)

solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;F. 重新排序&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2128/learning&lt;/p&gt;
&lt;p&gt;（感觉是这场最简单的一题。）&lt;/p&gt;
&lt;p&gt;第 $i$ 个元素，对最终和的贡献，取决于它处于几个区间内。如果处于 $c_i$ 个区间内，那最终贡献就是 $a_i \cdot c_i$&lt;/p&gt;
&lt;p&gt;我们可以 $O(N)$ 预处理出来第 $i$ 个位置处于区间个数的 $c_i$ 。（这种区间叠加，直接用差分即可）&lt;/p&gt;
&lt;p&gt;对于越大的 $c_i$ ，很显然匹配越大的 $a_i$，会使得结果最大。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys

lines = sys.stdin.read().splitlines()

n = int(lines[0].strip())
a = list(map(int, lines[1].split()))
m = int(lines[2].strip())
cnt = [0] * (n)
for i in range(m):
    l, r = map(int, lines[i+3].split())
    l -= 1
    r -= 1
    cnt[l] += 1
    if r+1 &amp;lt; n:
        cnt[r+1] -= 1
for i in range(1, n):
    cnt[i] += cnt[i-1]

ans = 0
for i in range(n):
    ans -= cnt[i] * a[i]

cnt = sorted(cnt)
a = sorted(a)
for i in range(n):
    ans += cnt[i] * a[i]
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;G. 全排列的价值&lt;/h2&gt;
&lt;p&gt;题目链接：https://www.lanqiao.cn/problems/2137/learning&lt;/p&gt;
&lt;p&gt;和前面一题一样，从贡献的角度切入。数 $i$，在排列中对这个排列价值的贡献就是它后面所有比它大的数的个数。全排列中，对于数 $j$（ $j&amp;gt;i$ ），会有多少次机会也就是有多少个排列，会排在 $i$ 后面？很显然是 $(n-2)!\cdot (C_{n-1}^2+n-1)$&lt;/p&gt;
&lt;p&gt;也就是说所，数 $i$ 对最后我们要输出的答案的贡献是
$$
\begin{aligned}
ans[i]&amp;amp;=\bigl( \sum_{j=i+1}^n (n-2)!\cdot (C_{n-1}^2+n-1) \bigr)\
&amp;amp;= (n-2)!\cdot (C_{n-1}^2+n-1) \cdot (n-i)\
\end{aligned}
$$
最后总的答案是
$$
\begin{aligned}
ans &amp;amp;= (n-2)!\cdot (C_{n-1}^2+n-1)\cdot \frac 1 2 n (n-1)\
&amp;amp;= \frac 1 4 n!\cdot n(n-1)
\end{aligned}
$$
然后需要注意，如果不写逆元，就需要特判一下两个 2 因子消掉即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys
import math

n = int(input())
m = 998244353

def fac_2(x):
    ret = 1
    for i in range(3, x + 1):
        ret *= i
        ret %= m
    return ret

ans = fac_2(n)
ans = (ans * (n * (n - 1) // 2 % m)) % m

print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;H. 最长不下降子序列&lt;/h2&gt;
&lt;p&gt;链接：https://www.lanqiao.cn/problems/2088/learning&lt;/p&gt;
&lt;h2&gt;I. 最优清零方案&lt;/h2&gt;
&lt;p&gt;链接：https://www.lanqiao.cn/problems/2138/learning&lt;/p&gt;
&lt;p&gt;很显然可以发现，我们只要先尽可能地 $k$ 个连续消掉，随便做，消到没办法再消，然后对剩下的逐个消除即可。&lt;/p&gt;
&lt;p&gt;因此直接 $O(n)$ 遍历一遍，遍历到 $i$ 时，对 $[i, i+k-1]$ 消掉 $\min_{j\in [i,i+k-1]}{a_j}$ 即可。正确性是有保证的。&lt;/p&gt;
&lt;p&gt;下面是暴力的写法，时间复杂度最坏 $O(nk)$.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import sys

n, k = map(int, input().split())
a = list(map(int, input().split()))

ok = -1
ans = 0
for i in range(n):
    if i &amp;lt;= ok or i+k &amp;gt; n:
        ans += a[i]
    else:
        j = i
        for p in range(i, i+k):
            if a[p] &amp;lt; a[j]:
                j = p
        h = a[j]
        ans += h
        for p in range(i, i+k):
            a[p] -= h
        ans += a[i]
        ok = j
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然这题时间复杂度非常坏，最坏会到 $O(nk)$，但是蓝桥杯官网上测试这段代码，是直接100%通过……&lt;/p&gt;
&lt;p&gt;但是还是看下怎么优化。&lt;/p&gt;
&lt;p&gt;上述的步骤，说简单就是对于 $i$，我们对 $[i,i+k)$ 这一个区间，全部减去区间中最小的 $a[j]$ ，然后 $i$ 就可以跳到 $j$ 了，继续下一步即可，可以看出主要的时间复杂度的问题都在寻找这个最小的 $a[j]$ 上和区间减法上了，因为朴素的做法中，一定要遍历 $k$ 个数才能找到最小的，在不打标记的情况下，区间减法也是要一个个减掉的。所以很自然会想到用线段树来实现区间减法和最小值寻找，这样时间复杂度就变成了 $O(n\log n)$， 对的。&lt;/p&gt;
&lt;p&gt;除此之外我感觉也可以用双端队列来做。但是写起来可能比线段树更难调，那不如直接线段树了。但是线段树会狠狠卡常，必须尽可能不搜就不搜。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys

n, k = map(int, sys.stdin.readline().split())
a = list(map(int, sys.stdin.readline().split()))
inf = 2e9
ans = sum(a)

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        self.tree = [[0, 0] for _ in range(4 * self.n)] 
        self.lazy = [0] * (4 * self.n)
        self._build(0, 0, self.n-1, data)
        
    def _build(self, u, l, r, data):
        if l == r:
            self.tree[u] = [data[l], l]
        else:
            mid = (l + r) // 2
            self._build(2*u+1, l, mid, data)
            self._build(2*u+2, mid+1, r, data)
            left_val, left_idx = self.tree[2*u+1]
            right_val, right_idx = self.tree[2*u+2]
            if left_val &amp;lt; right_val:
                self.tree[u] = [left_val, left_idx]
            else:
                self.tree[u] = [right_val, right_idx]

    def _query(self, u, l, r, L, R):
        if R &amp;lt; l or r &amp;lt; L:
            return [inf, -1] 
        if L &amp;lt;= l and r &amp;lt;= R:
            return self.tree[u]
        self._push(u)
        mid = (l + r) // 2
        left = self._query(2*u+1, l, mid, L, R)
        right = self._query(2*u+2, mid+1, r, L, R)
        return left if left[0] &amp;lt; right[0] else right

    def find_min(self, l, r):
        return self._query(0, 0, self.n-1, l, r)

    def _push(self, u):
        if self.lazy[u] != 0:
            self.tree[2*u+1][0] += self.lazy[u]
            self.lazy[2*u+1] += self.lazy[u]
            self.tree[2*u+2][0] += self.lazy[u]
            self.lazy[2*u+2] += self.lazy[u]
            self.lazy[u] = 0

    def _range_add(self, u, l, r, L, R, x):
        if R &amp;lt; l or r &amp;lt; L:
            return
        if L &amp;lt;= l and r &amp;lt;= R:
            self.tree[u][0] += x
            self.lazy[u] += x
            return
        self._push(u)
        mid = (l + r) // 2
        self._range_add(2*u+1, l, mid, L, R, x)
        self._range_add(2*u+2, mid+1, r, L, R, x)
        left_val, left_idx = self.tree[2*u+1]
        right_val, right_idx = self.tree[2*u+2]
        if left_val &amp;lt; right_val:
            self.tree[u] = [left_val, left_idx]
        else:
            self.tree[u] = [right_val, right_idx]

    def range_sub(self, l, r, x):
        self._range_add(0, 0, self.n-1, l, r, -x)

    def range_sum(self, u, l, r):
        if l == r:
            return self.tree[u][0] 
        self._push(u)
        mid = (l + r) // 2
        return self.range_sum(2*u+1, l, mid) + self.range_sum(2*u+2, mid+1, r)

segtree = SegmentTree(a)
p = -1
for i in range(n):
    if i + k &amp;lt;= n and i &amp;gt; p:
        min_val, min_idx = segtree.find_min(i, i + k - 1)
        segtree.range_sub(i, i + k - 1, min_val)
        p = min_idx
        ans -= k * min_val
        ans += min_val
print(ans)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;J. 数的拆分&lt;/h2&gt;
&lt;p&gt;链接：https://www.lanqiao.cn/problems/2090&lt;/p&gt;
</content:encoded></item><item><title>从零开始的汇编语言笔记（1）</title><link>https://fuwari.vercel.app/posts/course/assembly/assembly-note-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/assembly/assembly-note-1/</guid><pubDate>Thu, 02 Jan 2025 15:46:07 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;之前汇编课都没听，啥也不会，距离上机考试就剩几天了，希望能快速速成一下。下面是本人考前的速通笔记。&lt;/p&gt;
&lt;p&gt;（考完补充：本人上机结果还不错，全班第二个打完，可见汇编语言还是可以速通的！）&lt;/p&gt;
&lt;p&gt;（汇编的课本写得极其诡异，需要自己细品一下就能看懂。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;环境与DOSBOX&lt;/h2&gt;
&lt;h3&gt;启动、编辑、运行命令&lt;/h3&gt;
&lt;p&gt;（这里的命令只是用于我自己备忘，具体配置需自行处理。）&lt;/p&gt;
&lt;p&gt;启动命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount c: d:/masm
c:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑、编译运行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;edit &amp;lt;文件名&amp;gt;.asm
masm &amp;lt;文件名&amp;gt;.asm		# 汇编
link &amp;lt;文件名&amp;gt;.obj		# 链接
&amp;lt;文件名&amp;gt;.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;debug命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;debug program.exe
r						# 查看当前寄存器状态
r ax 

t						# 单步执行
d cs:0100				# 查看内存

u CS:IP					# 查看当前CS:IP开始的几行汇编代码

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DOSBOX&lt;/h3&gt;
&lt;p&gt;全屏/退出全屏：&lt;code&gt;alt+enter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;释放鼠标：&lt;code&gt;ctrl+F10&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;汇编语言入门：&lt;code&gt;a+b&lt;/code&gt;与&lt;code&gt;Hello world!&lt;/code&gt;&lt;/h2&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;h3&gt;&lt;code&gt;DB&lt;/code&gt;（字节，Define Byte）&lt;/h3&gt;
&lt;p&gt;格式是&lt;code&gt;DB &amp;lt;值1&amp;gt;, &amp;lt;值2&amp;gt;, ..., &amp;lt;值N&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myChar DB 65            ; 分配一个字节，并初始化为65（ASCII码的&apos;A&apos;）
myChars DB &apos;A&apos;, &apos;B&apos;, &apos;C&apos; ; 分配并初始化为三个字符
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;DW&lt;/code&gt;（字，Define Word）&lt;/h3&gt;
&lt;p&gt;格式是&lt;code&gt;DW &amp;lt;值1&amp;gt;, &amp;lt;值2&amp;gt;, ..., &amp;lt;值N&amp;gt;&lt;/code&gt;（汇编语言中，一个字通常是2个字节。）&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myWord DW 0x1234        ; 分配一个字，并初始化为16位数0x1234
myWords DW 0x1234, 0x5678 ; 分配两个字，并分别初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;寄存器的含义&lt;/h3&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;blockquote&gt;
&lt;p&gt;可以把寄存器视为用于工作的桌子，内存视为放材料的柜子。这个桌子的空间非常有限（因为寄存器的数量有限），但是访问非常快捷高效，就像你可以随手拿起桌子上的东西一样。内存则像是一个储存材料的柜子。它的空间相对较大，可以存放很多材料（数据和程序代码），但是从柜子中取材料或放回材料的速度没有桌子上操作那么快。程序运行时需要的数据大多数时间都是存放在这里的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;寄存器感觉是很多的，所以这里先掠过，后面细写。先记住最基础的数据寄存器&lt;code&gt;AX,BX,CX,DX&lt;/code&gt;这4个即可。&lt;/p&gt;
&lt;h3&gt;简单a+b程序、基础代码结构&lt;/h3&gt;
&lt;p&gt;下面是一个简单的a+b程序，主要逻辑仅仅使用了&lt;code&gt;AX&lt;/code&gt;寄存器。为了简单这里也没有任何直接输出的代码，因此运行结束后用debug来查看结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;;--------数据段开始----------
datasg segment
    a dw 5             ; 初始化 a 为 5
    b dw 3             ; 初始化 b 为 3
    result dw 0        ; 用于存储结果
datasg ends

;--------代码段开始---------
codesg segment
assume cs:codesg, ds:datasg
main proc far
    ; 初始化数据段寄存器
    push ds
    sub ax, ax
    push ax
    mov ax, datasg
    mov ds, ax

    ; 计算 a + b
    mov ax, [a]      ; 将 a 的值加载到 ax
    add ax, [b]      ; 将 b 的值添加到 ax
    mov [result], ax ; 将结果存储到 result 变量中

    ; 结束程序
    mov ax, 4C00h    ; 返回操作系统命令
    int 21h          ; 调用中断
main endp
codesg ends
end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;debug时，输入&lt;code&gt;u&lt;/code&gt;命令，查看当前代码段，然后一直&lt;code&gt;t&lt;/code&gt;，可以看见最后&lt;code&gt;AX&lt;/code&gt;变成了额&lt;code&gt;3+5=8&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;C:\Users\wlate\AppData\Roaming\Typora\typora-user-images\image-20241112024047268.png&quot; alt=&quot;image-20241112024047268&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;在上面可以看见&lt;code&gt;result&lt;/code&gt;的地址是&lt;code&gt;0004&lt;/code&gt;，下面查看这个位置的结果：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;C:\Users\wlate\AppData\Roaming\Typora\typora-user-images\image-20241112024646133.png&quot; alt=&quot;image-20241112024646133&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;这里列出了&lt;code&gt;ds:0004&lt;/code&gt;开始后面的所有结果，可以看出来&lt;code&gt;result&lt;/code&gt;确实是&lt;code&gt;08&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;重新来看上述的代码结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;;--------数据段开始----------
datasg segment
    a dw 5             ; 初始化 a 为 5
    b dw 3             ; 初始化 b 为 3
    result dw 0        ; 用于存储结果
datasg ends

;--------代码段开始---------
codesg segment
assume cs:codesg, ds:datasg
main proc far
    ; 初始化数据段寄存器
    push ds
    sub ax, ax
    push ax
    mov ax, datasg
    mov ds, ax

    ; 计算 a + b
    mov ax, [a]      ; 将 a 的值加载到 ax
    add ax, [b]      ; 将 b 的值添加到 ax
    mov [result], ax ; 将结果存储到 result 变量中

    ; 结束程序
    mov ax, 4C00h    ; 返回操作系统命令
    int 21h          ; 调用中断
main endp
codesg ends
end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了&lt;code&gt;datasg&lt;/code&gt;数据段外，主要关注代码段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codesg segment         ; 代码段开始
  assume cs:codesg, ds:datasg
  main proc far        ; 过程开始
    ; 程序指令
  main endp            ; 过程结束
codesg ends            ; 代码段结束
end main               ; 程序结束，入口点为 main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述指令中，&lt;code&gt;end&lt;/code&gt;告诉汇编器编译结束位置，并指定程序的入口点（起始执行位置）。&lt;/p&gt;
&lt;h2&gt;寄存器&lt;/h2&gt;
&lt;p&gt;&amp;lt;img src=&quot;D:\ProgramData\Tencent\1656996630\FileRecv\MobileFile\扫描件_存器名_001.jpg&quot; alt=&quot;扫描件_存器名_001&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;1.  通用寄存器&lt;/h3&gt;
&lt;h4&gt;数据寄存器&lt;/h4&gt;
&lt;p&gt;也就是前面的&lt;code&gt;AX,BX,CX,DX&lt;/code&gt;四个寄存器，每个是十六位的，也就是两个字节，也就是一个字。&lt;/p&gt;
&lt;h4&gt;间接寻址寄存器&lt;/h4&gt;
&lt;p&gt;两个基址寄存器&lt;code&gt;BX&lt;/code&gt;、&lt;code&gt;BP&lt;/code&gt;和两个变址寄存器&lt;code&gt;SI&lt;/code&gt;、&lt;code&gt;DI&lt;/code&gt;构成。&lt;/p&gt;
&lt;p&gt;除了可以用来存放要访问的内存单元的偏移地址外，还可以作为通用寄存器使用，来存放操作数和计算结果。和数据寄存器在这一点上没差别。&lt;/p&gt;
&lt;p&gt;（一般来讲，&lt;code&gt;SI&lt;/code&gt;用于源数据的索引，指向要读取的数据。&lt;code&gt;DI&lt;/code&gt;作为目标数据索引，指向要写入的数据。）&lt;/p&gt;
&lt;h4&gt;指针寄存器&lt;/h4&gt;
&lt;p&gt;堆栈指针&lt;code&gt;SP&lt;/code&gt;和基指针&lt;code&gt;BP&lt;/code&gt;。（Stack Pointer, Base Pointer）&lt;/p&gt;
&lt;p&gt;也可以作为通用寄存器使用，来存放操作数和计算结果。和数据寄存器在这一点上没差别。&lt;/p&gt;
&lt;h3&gt;2.  段寄存器&lt;/h3&gt;
&lt;p&gt;我们知道，CPU会把主存划分为若干段，把要运行的程序各部分放在不同的存储段中，每个存储段用一个段寄存器来指示首地址。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;D:\ProgramData\Tencent\1656996630\FileRecv\MobileFile\Image_1731435246745.jpg&quot; alt=&quot;Image_1731435246745&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CS&lt;/code&gt;：代码段寄存器，用于指向包含程序执行代码的段。指令指针寄存器 &lt;code&gt;IP&lt;/code&gt;（或在更高级的模式下的 &lt;code&gt;EIP&lt;/code&gt;、&lt;code&gt;RIP&lt;/code&gt;）与 &lt;code&gt;CS&lt;/code&gt; 一起决定了当前执行指令的完整地址。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DS&lt;/code&gt;：数据段寄存器，用于指向程序中适用的大部分变量数据所在的段。在访问内存数据时，没有指定段寄存器，CPU 通常会使用 &lt;code&gt;DS&lt;/code&gt; 作为默认寄存器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ES&lt;/code&gt;：附加段寄存器。通常用于在数据传输中作为一个附加的段寄存器，特别是在执行字符串和其他大块数据操作时，它可以作为源地址或目标地址的段。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SS&lt;/code&gt;：堆栈段寄存器。堆栈用于存储局部变量、返回地址、历史寄存器状态等。堆栈指针寄存器 &lt;code&gt;SP&lt;/code&gt;（或 &lt;code&gt;ESP&lt;/code&gt;, &lt;code&gt;RSP&lt;/code&gt;）和 &lt;code&gt;SS&lt;/code&gt; 一起决定了当前堆栈的完整地址。&lt;/p&gt;
&lt;p&gt;一般来讲，初学时，最重要的是&lt;code&gt;CS&lt;/code&gt;和&lt;code&gt;DS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在前面的a+b程序中，可以看见这二者是怎么被设置的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;;--------数据段开始----------
datasg segment
    a dw 5    
    b dw 3      
    result dw 0     
datasg ends

;--------代码段开始---------
codesg segment
assume cs:codesg, ds:datasg
main proc far
    ; 初始化数据段寄存器
    push ds
    sub ax, ax
    push ax
    mov ax, datasg
    mov ds, ax

    ; 计算 a + b
    mov ax, [a]      ; 将 a 的值加载到 ax
    add ax, [b]      ; 将 b 的值添加到 ax
    mov [result], ax ; 将结果存储到 result 变量中

    ; 结束程序
    mov ax, 4C00h    ; 返回操作系统命令
    int 21h          ; 调用中断
main endp
codesg ends
end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述程序中，这句话&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;assume cs:codesg, ds:datasg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;告诉了汇编器段寄存器（如 &lt;code&gt;CS&lt;/code&gt;, &lt;code&gt;DS&lt;/code&gt;, &lt;code&gt;ES&lt;/code&gt;, &lt;code&gt;SS&lt;/code&gt;）当前假设（assume）指向的段。这里，当前的 &lt;code&gt;CS&lt;/code&gt; 寄存器被假设为指向 &lt;code&gt;codesg&lt;/code&gt; 段，而 &lt;code&gt;DS&lt;/code&gt; 被假设为指向 &lt;code&gt;datasg&lt;/code&gt; 段。&lt;/p&gt;
&lt;p&gt;但是，实际上，尽管 &lt;code&gt;assume&lt;/code&gt; 指令对汇编器的地址解析非常有用，但它并不会改变或设置处理器中实际的段寄存器值。（仅仅只是一个声明！）&lt;/p&gt;
&lt;p&gt;因此，当程序在 CPU 上运行时，实际的段寄存器需要通过指令显式设置。也就是后面的语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	mov ax, datasg
    mov ds, ax

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CS&lt;/code&gt;作为代码段寄存器，与其他段寄存器不同，它是不需要显式设置的，我们最后的&lt;code&gt;end main&lt;/code&gt;，不仅规定了程序的结束点，也规定了程序的起点&lt;code&gt;main&lt;/code&gt;。操作系统在加载程序到内存并执行时，会自动将代码段寄存器（CS）设置为对应的值，确保执行从 &lt;code&gt;main&lt;/code&gt; 开始。因此，程序员不需要（也不应该）在程序中手动设置 &lt;code&gt;CS&lt;/code&gt; 寄存器，&lt;/p&gt;
&lt;p&gt;但是&lt;code&gt;DS&lt;/code&gt;寄存器不同。这句话&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    mov ax, datasg
    mov ds, ax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;code&gt;ds&lt;/code&gt;设置成了&lt;code&gt;datasg&lt;/code&gt;的地址。为什么要用&lt;code&gt;ax&lt;/code&gt;做中间的寄存器？因为在x86架构中，段寄存器（如DS、ES、SS等）不能直接接收一个立即数（即直接编码在指令中的数值）或内存地址作为操作数。这就需要通用寄存器来中转。&lt;/p&gt;
&lt;p&gt;除此之外，&lt;code&gt;ss&lt;/code&gt;（堆栈段寄存器）也是无需显式设置的，在大多数操作环境中，尤其是在现代操作系统上运行的程序，操作系统在启动任何程序之前已经为其配置好了堆栈。这包括设置&lt;code&gt;SS&lt;/code&gt;（堆栈段寄存器）和&lt;code&gt;SP&lt;/code&gt;（堆栈指针寄存器）。&lt;/p&gt;
&lt;h3&gt;3.  指令指针IP&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;IP&lt;/code&gt;是程序不能访问的寄存器。是指令的指针地址，在程序运行期间，CPU会自动修改IP的值，使得它始终是正在执行指令的下一条指令代码的起始字节的偏移量。&lt;/p&gt;
&lt;p&gt;有些指令可以自动修改&lt;code&gt;IP&lt;/code&gt;的值，例如&lt;code&gt;JMP&lt;/code&gt;，&lt;code&gt;JNE&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;执行调用子程序指令&lt;code&gt;CALL&lt;/code&gt;时，&lt;code&gt;IP&lt;/code&gt;原有内容自动压入堆栈，把子程序入口地址偏移量自动送入&lt;code&gt;IP&lt;/code&gt;。从子程序返回主程序时，返回指令&lt;code&gt;RET&lt;/code&gt;又会自动从堆栈弹出原有&lt;code&gt;IP&lt;/code&gt;的内容送回&lt;code&gt;IP&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;4.  标志寄存器&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/%E6%89%AB%E6%8F%8F%E4%BB%B6_%E6%8E%A7%E5%88%B6%E5%AF%84%E5%AD%98%E5%99%A8_001.jpg&quot; alt=&quot;扫描件_控制寄存器_001&quot; style=&quot;zoom: 67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;进位标志CF：最高位产生进位或借位，或者位移指令中最低位和最高位移出的代码。&lt;/p&gt;
&lt;p&gt;奇偶标志：操作结果含1是偶数，则是1。&lt;/p&gt;
&lt;p&gt;辅助进位AF：低半字节和高半字节产生进位或借位。只和低八位有关。&lt;/p&gt;
&lt;p&gt;符号标志：与结果的最高位有关。&lt;/p&gt;
&lt;h2&gt;指令系统&lt;/h2&gt;
&lt;p&gt;80x86的指令系统可以分为：数据传送指令、算数指令串处理指令、&lt;/p&gt;
&lt;h3&gt;（一）数据传送指令&lt;/h3&gt;
&lt;h4&gt;1. 通用数据传送指令：&lt;code&gt;MOV&lt;/code&gt;、&lt;code&gt;PUSH&lt;/code&gt;、&lt;code&gt;POP&lt;/code&gt;、&lt;code&gt;XCHG&lt;/code&gt;&lt;/h4&gt;
&lt;h5&gt;① &lt;code&gt;MOV&lt;/code&gt;&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MOV&lt;/code&gt;等双操作数指令，不允许两个操作数都使用存储器，必须至少有一个是寄存器。&lt;/p&gt;
&lt;p&gt;立即数和内存地址，不能直接传入段寄存器，只能是寄存器传入。例如，我们前面程序这句话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov ax, datasg
mov ds, ax
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MOV&lt;/code&gt;不影响标志位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;MOV		BX, OFFSET TABLE	; 把偏移地址传入BX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MOVSX&lt;/code&gt;和&lt;code&gt;MOVZX&lt;/code&gt;是带符号和带零扩展指令。&lt;/p&gt;
&lt;h5&gt;② &lt;code&gt;PUSH&lt;/code&gt;、&lt;code&gt;POP&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;假设我们有一个主程序，需要调用一个子程序来计算两个数的和。在调用子程序之前，主程序可能会使用某些寄存器，而子程序也需要使用寄存器来进行计算。为了避免子程序的操作影响到主程序中寄存器的值，我们需要在调用子程序之前保存寄存器的状态，并在子程序返回后恢复这些寄存器。这时，&lt;code&gt;PUSH&lt;/code&gt; 和 &lt;code&gt;POP&lt;/code&gt; 指令就显得非常重要。&lt;/p&gt;
&lt;h4&gt;2. 累加器专用传送指令：&lt;code&gt;IN&lt;/code&gt;、&lt;code&gt;OUT&lt;/code&gt;、&lt;code&gt;XLAT&lt;/code&gt;&lt;/h4&gt;
&lt;h5&gt;① &lt;code&gt;IN&lt;/code&gt;、&lt;code&gt;OUT&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;IN&lt;/code&gt; 指令&lt;/strong&gt;：从I/O端口读取数据到累加器（&lt;code&gt;AL&lt;/code&gt; 或 &lt;code&gt;AX&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;OUT&lt;/code&gt; 指令&lt;/strong&gt;：将累加器中的数据（&lt;code&gt;AL&lt;/code&gt; 或 &lt;code&gt;AX&lt;/code&gt;）写入到I/O端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IN AL, DX    ; 从端口地址由DX指定的I/O端口读取一个字节到AL
IN AX, DX    ; 从端口地址由DX指定的I/O端口读取两个字节到AX

OUT DX, AL   ; 将AL中的一个字节数据写入端口地址由DX指定的I/O端口
OUT DX, AX   ; 将AX中的两个字节数据写入端口地址由DX指定的I/O端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;I/O操作专用&lt;/strong&gt;：&lt;code&gt;IN&lt;/code&gt; 和 &lt;code&gt;OUT&lt;/code&gt; 是专门用于与硬件设备的I/O端口进行通信的指令。这些指令直接与CPU的I/O总线交互，而 &lt;code&gt;MOV&lt;/code&gt; 指令只能在CPU内部或内存与寄存器之间传输数据，无法直接与I/O端口通信。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬件交互&lt;/strong&gt;：&lt;code&gt;IN&lt;/code&gt; 和 &lt;code&gt;OUT&lt;/code&gt; 允许程序与外部设备（如键盘、显示器、硬盘控制器等）进行数据交换，这是 &lt;code&gt;MOV&lt;/code&gt; 无法实现的功能。&lt;/p&gt;
&lt;h5&gt;② &lt;code&gt;XLAT&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Translate&lt;/p&gt;
&lt;p&gt;&lt;code&gt;XLAT&lt;/code&gt;（&lt;strong&gt;Translate&lt;/strong&gt;）指令用于将一个字节的值从查找表中翻译为另一个值。它通过 &lt;code&gt;AL&lt;/code&gt; 寄存器中的索引值，从查找表（由 &lt;code&gt;BX&lt;/code&gt; 寄存器指向）中加载对应的字节，并将其存储回 &lt;code&gt;AL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在使用前，应该先建立一个字节表格，表格首地址提前存入&lt;code&gt;BX&lt;/code&gt;或&lt;code&gt;EBX&lt;/code&gt;。需要转换的代码（也就是相对于首地址的偏移量）存入&lt;code&gt;AL&lt;/code&gt;寄存器。&lt;/p&gt;
&lt;h4&gt;3. 地址传送指令&lt;/h4&gt;
&lt;h5&gt;① &lt;code&gt;LEA&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;把源操作数的有效地址送到指定寄存器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LEA	BX, [BX+SI+0F62H]
; 执行后 (BX)=(BX)+(SI)+0F62H

LEA	BX, LIST
MOV BX, OFFSET LIST
; 上述是等价的
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;② &lt;code&gt;LDS&lt;/code&gt;、&lt;code&gt;LES&lt;/code&gt;....&lt;/h5&gt;
&lt;h4&gt;4. 标志寄存器传送指令&lt;/h4&gt;
&lt;h4&gt;5. 类型转换指令：&lt;code&gt;CBW&lt;/code&gt;、&lt;code&gt;CWD/CWDE&lt;/code&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;本组指令不影响标志位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;（二）算数指令&lt;/h3&gt;
&lt;h4&gt;① &lt;code&gt;ADD&lt;/code&gt;、&lt;code&gt;SUB&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;除此之外还有&lt;code&gt;ADC&lt;/code&gt;（带进位加法），&lt;code&gt;DST &amp;lt;- SRC + DST + CF&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SBB&lt;/code&gt;（带借位减法），&lt;code&gt;DST &amp;lt;- DST - SRC - CF&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;② &lt;code&gt;INC&lt;/code&gt;、&lt;code&gt;DEC&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;INC&lt;/code&gt;不影响&lt;code&gt;CF&lt;/code&gt;标志位。&lt;/p&gt;
&lt;h4&gt;③ &lt;code&gt;NEG	&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;求补。也就是取反后+1.&lt;/p&gt;
&lt;h4&gt;④ &lt;code&gt;CMP&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;进行减法计算，但不保留结果，只会根据结果设置条件标志位。可以在后面跟上条件转移指令，比如&lt;code&gt;JMP&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;⑤ &lt;code&gt;MUL&lt;/code&gt;、&lt;code&gt;IMUL&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MUL	SRC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存的结果会自动扩两倍。&lt;/p&gt;
&lt;p&gt;字节操作数：&lt;code&gt;(AX) &amp;lt;- (AL) * (SRC)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;字操作数：&lt;code&gt;(DX, AX) &amp;lt;- (AX) * (SRC)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;双字操作数：&lt;code&gt;(EDX, EAX) &amp;lt;- (EAX) * (SRC)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IMUL&lt;/code&gt;是带符号数乘法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MUL&lt;/code&gt;和&lt;code&gt;IMUL&lt;/code&gt;指令可以通过&lt;code&gt;CF&lt;/code&gt;和&lt;code&gt;OF&lt;/code&gt;均为0来判断扩展出来的字或字节是否有被用到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;⑥ &lt;code&gt;DIV&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;DIV	SRC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;code&gt;SRC&lt;/code&gt;的长度来分配2倍长度的&lt;code&gt;AX&lt;/code&gt;或&lt;code&gt;(DX, AX)&lt;/code&gt;或&lt;code&gt;(EDX, EAX)&lt;/code&gt;做被除数。&lt;/p&gt;
&lt;p&gt;商存在与&lt;code&gt;SRC&lt;/code&gt;同样长度的&lt;code&gt;AL&lt;/code&gt;或&lt;code&gt;AX&lt;/code&gt;或&lt;code&gt;EAX&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;⑦ 十进制调整指令&lt;/h4&gt;
&lt;p&gt;我们知道BCD码是用二进制编码的十进制。压缩的BCD码是我们常见的那种，4位为一个数字。非压缩的BCD码，一个数字也是一个字节，低4位表示数值，高4位没有意义。因此ASCII码也是一种非压缩的BCD码，高4位为&lt;code&gt;0011&lt;/code&gt;，低4位是数值。&lt;/p&gt;
&lt;h3&gt;（三）逻辑指令&lt;/h3&gt;
&lt;h4&gt;① &lt;code&gt;AND&lt;/code&gt;、&lt;code&gt;OR&lt;/code&gt;、&lt;code&gt;NOT&lt;/code&gt;、&lt;code&gt;XOR&lt;/code&gt;、&lt;code&gt;TEST&lt;/code&gt;&lt;/h4&gt;
&lt;h4&gt;② &lt;code&gt;SHL&lt;/code&gt;、&lt;code&gt;SAL&lt;/code&gt;（逻辑和算术左移），&lt;code&gt;SHR&lt;/code&gt;、&lt;code&gt;SAR&lt;/code&gt;（逻辑和算术右移）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;SHL	OPR, CL/1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;③ &lt;code&gt;ROL&lt;/code&gt;、&lt;code&gt;ROR&lt;/code&gt;（循环左右移动），&lt;code&gt;RCL&lt;/code&gt;、&lt;code&gt;RCR&lt;/code&gt;（带&lt;code&gt;CF&lt;/code&gt;进位）&lt;/h4&gt;
&lt;h3&gt;（四）串处理指令&lt;/h3&gt;
&lt;p&gt;在汇编语言中，“串”（String）通常指的是一系列连续存储的字符数据。串在汇编程序中的处理方式稍有不同于高级编程语言，但基本概念类似。串可以是任何长度，通常由程序员定义，并且可以包括字符、数字和其他数据类型。在 x86 汇编中，串操作是通过一组特定的指令来实现的，这些指令专门用于处理内存中的连续字节序列。&lt;/p&gt;
&lt;h4&gt;① &lt;code&gt;MOVS&lt;/code&gt;：串移动&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在数据段中有 一个字符串，要求把它转送到附加段的缓冲区中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    mess1   db  &apos;Hello world!&apos;, &apos;$&apos; ; 长度为13
dataseg ends

extra segment
    mess2   db  30 dup (?)
extra ends

codeseg segment
    assume cs: codeseg, ds: dataseg, es: extra
main proc far
    push ds
    sub ax, ax
    push ax
    
    mov ax, dataseg
    mov ds, ax

    mov ax, extra
    mov es, ax

    lea si, mess1
    lea di, mess2
    mov cx, 13
    cld
    rep movsb

    pop ax  
    pop ds

    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想把字符串反向移动过去呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在数据段中有 一个字符串，要求把它反向后，转送到附加段的缓冲区中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里因为标志寄存器是同时影响&lt;code&gt;di&lt;/code&gt;和&lt;code&gt;si&lt;/code&gt;的，所以需要手动写一个循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    mess1   db  &apos;Hello world!&apos;, &apos;$&apos; ; 长度为13，实际使用12字符
dataseg ends

extra segment
    mess2   db  30 dup (?)  ; 为目标字符串预留空间
extra ends

codeseg segment
    assume cs: codeseg, ds: dataseg, es: extra
main proc far
    push ds
    sub ax, ax
    push ax
    
    mov ax, dataseg
    mov ds, ax

    mov ax, extra
    mov es, ax

    lea si, [mess1 + 11]  ; 将 SI 初始化为源字符串的最后一个有效字符 (&apos;!&apos;)
    lea di, mess2         ; 将 DI 初始化为目标区域的开始
    mov cx, 12            ; 设置计数器为有效字符长度，不包括结尾的 &apos;$&apos;

reverse_copy_loop:
    mov al,     [si]
    mov es:[di],   al
    dec si
    inc di  
    loop reverse_copy_loop

    pop ax  
    pop ds

    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241124213703925.png&quot; alt=&quot;image-20241124213703925&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;如果是字移动，也一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    mess1   dw 1, 2, 3, 4, 5    ; 定义一个包含5个字的数组
dataseg ends

extra segment
    mess2   dw 5 dup (?)        ; 为目标数组预留5个字的空间
extra ends

codeseg segment
    assume cs: codeseg, ds: dataseg, es: extra
main proc far
    push ds
    xor ax, ax
    push ax
    
    mov ax, dataseg
    mov ds, ax

    mov ax, extra
    mov es, ax

    lea si, mess1 + 8           ; mess1 + (5-1)*2 = mess1 + 8，指向最后一个字
    lea di, mess2               

    mov cx, 5                   

reverse_copy_loop:
    mov ax, [si]                ; 从 DS:[SI] 读取一个字到 AX
    mov es:[di], ax             ; 将 AX 中的字存入 ES:[DI]
    sub si, 2                   ; SI 递减2，指向前一个字
    add di, 2                   ; DI 递增2，指向下一个目标位置
    loop reverse_copy_loop       ; CX 减1，若不为0则继续循环

    pop ax  
    pop ds

    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241124214400831.png&quot; alt=&quot;image-20241124214400831&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h4&gt;② &lt;code&gt;STOS&lt;/code&gt;、&lt;code&gt;LODS&lt;/code&gt;：串赋值、串取值&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;STOS	DST
STOSB
STOSW
STOSD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以将&lt;code&gt;DST&lt;/code&gt;地址赋值为&lt;code&gt;AL&lt;/code&gt;或&lt;code&gt;AX&lt;/code&gt;（是什么取决于&lt;code&gt;STOSB&lt;/code&gt;或&lt;code&gt;STOSW&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LODSB  ; 将 [DS:SI] 指向的一个字节加载到 AL 中，并把 SI + 1 或 -1
LODSW  ; 将 [DS:SI] 指向的一个字加载到 AX 中，并把 SI + 2 或 - 2
LODSD  ; 将 [DS:SI] 指向的一个双字加载到 EAX 中，并把 SI + 4 或 - 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来讲，这个不和&lt;code&gt;REP&lt;/code&gt;联用，可以用于逐次取出测试时。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    mess1   dw 1, 3, 5, 7, 9    ; 定义一个包含5个字的数组
dataseg ends

extra segment
    mess2   dw 5 dup (?)        ; 为目标数组预留5个字的空间
extra ends

codeseg segment
    assume cs: codeseg, ds: dataseg, es: extra
main proc far
    push ds
    xor ax, ax
    push ax
    
    mov ax, dataseg
    mov ds, ax

    mov ax, extra
    mov es, ax

    lea si, mess1 + 8           ; mess1 + (5-1)*2 = mess1 + 8，指向最后一个字
    lea di, mess2               

    mov cx, 5
reverse_copy_loop:
    std
    lodsw
    cld
    stosw
    loop reverse_copy_loop

    pop ax  
    pop ds

    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241125103351086.png&quot; alt=&quot;image-20241125103351086&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h4&gt;③ &lt;code&gt;REPE&lt;/code&gt;、&lt;code&gt;CMPS&lt;/code&gt;、&lt;code&gt;SCAS&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;REPE&lt;/code&gt;与&lt;code&gt;REP&lt;/code&gt;几乎一致，只是增加了除了&lt;code&gt;Count Reg=0&lt;/code&gt;的结束条件外，&lt;code&gt;ZF!=0&lt;/code&gt;也可以结束条件（也就是说，两数相等就可以继续比较，不等就提前结束）。除此之外，还可以写作&lt;code&gt;REPZ&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个汇编程序，在一个较大的目标字符串 &lt;code&gt;target&lt;/code&gt; 中查找子字符串 &lt;code&gt;pattern&lt;/code&gt; 的位置。若找到，返回子字符串在目标字符串中的起始索引；否则，返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;示例数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data_segment segment
    target  db &apos;The quick brown fox jumps over the lazy dog&apos;,0
    pattern db &apos;brown&apos;,0
    index   dw ?
data_segment ends
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    target  db &apos;The brown fox.&apos;,&apos;$&apos;
    pattern db &apos;brown&apos;,&apos;$&apos;
    index   dw -1
 
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es: dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov ax, dataseg
    mov es, ax

    lea bx, target
    ; 这里没限制cx可能会有点问题，后面想起来
search:
    mov si, bx
    lea di, pattern
    mov cx, 5
    repe cmpsb
    je is_match
    inc bx

    cmp byte ptr [bx], &apos;$&apos;
    jz over

    jmp search

is_match:
    mov index, bx
over:
    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241125132113928.png&quot; alt=&quot;image-20241125132113928&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个汇编程序，在一个较大的目标字符串 &lt;code&gt;target&lt;/code&gt; 中查找字符 &lt;code&gt;char_count&lt;/code&gt; 的出现次数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    target          db  &apos;The quick brown fox jumps over the lazy dog&apos;, &apos;$&apos;
    char_count      db  &apos;o&apos;
    count           dw  0
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es: dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov ax, dataseg
    mov es, ax

    mov al, char_count
    lea di, target
    mov cx, [char_count - target]
    mov bx, 0

search_loop:
    repne scasb
    jne over
    inc bx
    jmp search_loop

over:
    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241125150144524.png&quot; alt=&quot;image-20241125150144524&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;（五）控制转移指令&lt;/h3&gt;
&lt;h3&gt;（六）子程序&lt;/h3&gt;
&lt;h4&gt;① 基本结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;; 主程序段
main proc
    ; 调用子程序
    call Subroutine

    ; 其他代码

    ; 程序结束
    mov ax, 4C00h
    int 21h
main endp

; 子程序段
Subroutine proc
    ; 子程序的代码
    ; ...

    ; 返回主程序
    ret
Subroutine endp

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;② 参数传递&lt;/h4&gt;
&lt;p&gt;可以通过寄存器传递、堆栈传递、内存地址传递、&lt;/p&gt;
&lt;h3&gt;（七）中断&lt;/h3&gt;
&lt;p&gt;中断中最常用的是&lt;code&gt;int 21h&lt;/code&gt;，是DOS提供的主要中断，用于执行各种系统调用，文件操作、设备输入输出、内存管理等。具体功能由 &lt;code&gt;AH&lt;/code&gt; 寄存器的值指定。&lt;/p&gt;
&lt;h4&gt;① 打印字符到屏幕&lt;/h4&gt;
&lt;p&gt;在前面都是用&lt;code&gt;debug&lt;/code&gt;命令调试来看寄存器值，这里可以用这个中断打印到屏幕。&lt;/p&gt;
&lt;p&gt;寄存器设置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AH = 09h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DX&lt;/code&gt; 指向以&lt;code&gt;$&lt;/code&gt;结尾的字符串&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    message db &apos;Hello world!&apos;, &apos;$&apos;
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax

    mov ah, 09h
    lea dx, message
    int 21h

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241125180935561.png&quot; alt=&quot;image-20241125180935561&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h4&gt;② 从标准输入读取字符&lt;/h4&gt;
&lt;p&gt;寄存器设置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AH=01h&lt;/code&gt;，返回值是&lt;code&gt;AL&lt;/code&gt;，等于读取的字符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AH=0Ah&lt;/code&gt;，读取一个字符串，需要特定的缓冲区：第一个字节是缓冲区的最大字符数，第二个字节由DOS填充，是实际输入的字符数，后续字节用于存储输入的字符。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面做一个输出输入字符串的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    prompt_input db &apos;Enter a string: &apos;, &apos;$&apos;
    buffer_input db 50
    buffer_input_len    db 0
    buffer_input_data   db 50 dup(?)

    prompt_output db &apos;You entered: &apos;, &apos;$&apos;
    
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax

    ; 打印输入提示
    mov ah, 09h
    lea dx, prompt_input
    int 21h
    ; 读取输入
    mov ah, 0Ah
    lea dx, buffer_input
    int 21h

    call newline

    ; 打印输出提示
    mov ah, 09h
    lea dx, prompt_output
    int 21h
    ; 添加 $
    lea si, buffer_input_data
    xor bx, bx
    mov bl, buffer_input_len
    add si, bx
    mov byte ptr [si], &apos;$&apos;
    ; 打印字符串
    mov ah, 09h
    lea dx, buffer_input_data
    int 21h

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp

newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241126021440663.png&quot; alt=&quot;image-20241126021440663&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;系统功能调用&lt;/h2&gt;
&lt;h3&gt;中断&lt;/h3&gt;
&lt;p&gt;中断分为硬件中断和软件中断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬件中断&lt;/strong&gt;：由硬件设备(如键盘、定时器等)发出信号，CPU 响应中断并跳转到相应的中断服务程序(Interrupt Service Routine, ISR)，来处理硬件事件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件中断&lt;/strong&gt;：由软件使用 &lt;code&gt;INT n&lt;/code&gt; 指令主动触发，并由操作系统或 BIOS 提供的中断向量来执行特定功能。例如在 DOS 下常见的 &lt;code&gt;INT 21h&lt;/code&gt;，可用于文件操作、控制台输入输出等。这些指令实际上是“系统提供的功能接口”。&lt;/p&gt;
&lt;p&gt;在 8086 环境中，「系统功能调用」通常是通过软件中断（Software Interrupt）来完成的。&lt;/p&gt;
&lt;p&gt;在现代操作系统中，系统功能调用往往是一种“用户态向内核态请求服务”的方法。它可以通过多种技术实现，比如特殊指令 (如 Linux 的 &lt;code&gt;syscall&lt;/code&gt;)、中断指令 (如早期 DOS、BIOS 环境下的 &lt;code&gt;INT 21h&lt;/code&gt;、&lt;code&gt;INT 10h&lt;/code&gt; 等)。&lt;/p&gt;
&lt;p&gt;对于 8086/DOS 编程时代，“系统功能调用”大多数情况下就是通过软件中断（比如 &lt;code&gt;INT 21h&lt;/code&gt;）来执行的，也就是说想要访问操作系统提供的功能（读写文件、输出字符串、获取系统时间等），就会写一个 &lt;code&gt;INT 21h&lt;/code&gt;，并通过寄存器传递不同的功能号和参数。&lt;/p&gt;
&lt;p&gt;所以在 DOS/BIOS 里常见的系统服务（读写磁盘、文本输出等）需要用 &lt;code&gt;INT n&lt;/code&gt; 指令才能实现，这和 Linux/Windows 里的系统调用机制在概念上相似，只不过实现方式和调用约定不同。&lt;/p&gt;
&lt;h3&gt;程序直接控制I/O&lt;/h3&gt;
&lt;p&gt;外设接口由一组寄存器（数据寄存器/状态寄存器/命令寄存器）组成，每个寄存器有一个端口地址(端口号)。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;image-20250102095041748&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;I/O指令&lt;/h4&gt;
&lt;p&gt;在 8086 体系结构中，硬件设备可以通过“端口（I/O port）”来与 CPU 交互。&lt;/p&gt;
&lt;p&gt;8086 CPU 在汇编层面提供了 &lt;code&gt;IN&lt;/code&gt;/&lt;code&gt;OUT&lt;/code&gt; 指令用于访问 I/O 端口，原理上它与访问内存的 &lt;code&gt;MOV&lt;/code&gt; 指令是不同的总线周期。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AL&lt;/code&gt; 是 8 位寄存器，&lt;code&gt;AX&lt;/code&gt; 是 16 位寄存器。&lt;/p&gt;
&lt;p&gt;当我们执行 &lt;code&gt;IN AL, port&lt;/code&gt;，表示从端口读取 8 位数据放到 AL；如果是 &lt;code&gt;IN AX, port&lt;/code&gt;，则读取 16 位数据放到 AX（相当于先读端口低字节到 AL，再读端口高字节到 AH）。&lt;/p&gt;
&lt;p&gt;类似地，&lt;code&gt;OUT port, AL&lt;/code&gt; 是输出 8 位；&lt;code&gt;OUT port, AX&lt;/code&gt; 是输出 16 位。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PORT&lt;/code&gt; 是一个&lt;strong&gt;在指令中直接写死的 8 位立即数&lt;/strong&gt;（只支持 0~255 范围内的端口号）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DX&lt;/code&gt; 可以在程序中通过 &lt;code&gt;mov dx, SomePortValue&lt;/code&gt; 动态设置更大的端口号（0~65535）。&lt;/p&gt;
&lt;p&gt;当端口号大于 255 时，必须用 &lt;code&gt;DX&lt;/code&gt; 传递端口号，而不能直接用 &lt;code&gt;PORT&lt;/code&gt; 立即数。&lt;/p&gt;
&lt;h4&gt;示例：控制喇叭&lt;/h4&gt;
&lt;p&gt;&amp;lt;img src=&quot;C:\Users\wlate\AppData\Roaming\Typora\typora-user-images\image-20250102102258868.png&quot; alt=&quot;image-20250102102258868&quot; style=&quot;zoom: 40%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 读端口0x61，然后翻转bit1产生方波
mov dx, 61h        ; 端口0x61放到DX
in  al, dx         ; AL ← (0x61)
xor al, 00000010b  ; 翻转第1位
out dx, al         ; (0x61) ← AL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整喇叭控制代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
dataseg ends

codeseg segment
assume cs:codeseg, ds:dataseg

main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax

    ; --- 发声循环开始 ---
    mov dx, 100h         ; 控制循环次数
    in  al, 61h          ; 读 0x61 端口
    and al, 11111100b    ; 先清除 bit0/bit1，关闭喇叭
    out 61h, al

sound_loop:
    xor al, 00000010b    ; 翻转 bit1 (产生方波)
    out 61h, al
    mov cx, 140h         ; 延时，影响音调
delay1:
    loop delay1

    dec dx
    jne sound_loop
    ; --- 发声循环结束 ---

    ; 退出前关掉喇叭
    in  al, 61h
    and al, 11111100b
    out 61h, al

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp
codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;示例：打印字符&lt;/h4&gt;
&lt;p&gt;&amp;lt;img src=&quot;C:\Users\wlate\AppData\Roaming\Typora\typora-user-images\image-20250102102340257.png&quot; alt=&quot;image-20250102102340257&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;数据寄存器的端口地址为&lt;code&gt;378H&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start:
	mov   	si , offset mess	;message offset
	mov  	cx , count         	;count of char
next:
	mov  	dx , 379h   		;状态寄存器
wait1:
	        in      	al , dx                       
        	test    	al , 80h              	;最高位是否为1
       	    je      	wait1                       	;yes, test again
        	mov   	al , [si]              	;no, read a char.
        	mov   	dx , 378h        	;数据寄存器
        	out     	dx , al                      	;putout to data port
        	mov  	dx , 37ah          	;控制寄存器
        	mov  	al , 0dh                	;00001101
        	out     	dx , al                   	;send a strobe = 1
        	mov  	al , 0ch                	;00001100
        	out     	dx , al                    	;strobe = 0
        	inc     	si                             	;addr. of mess increment
        	loop  	next                 	;next char
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;;---------------------------------------------
; 简要说明：
; 1) 本代码示范如何通过并行端口 LPT1 (0x378 ~ 0x37F) 
;    输出数据（字符），并使用状态寄存器(0x379)、控制寄存器(0x37A)
; 2) 假设 LPT1 的地址为 0x378
;    - 数据寄存器：0x378
;    - 状态寄存器：0x379 (只读)
;    - 控制寄存器：0x37A (读/写)
; 3) 打印流程简化：先读状态(忙碌位?)，如果忙则等待，不忙则输出数据并“触发选通(strobe)”
;---------------------------------------------

dataseg segment
    ; 待输出字符串（示例）
    mess db &apos;Hello LPT1!&apos;, 0Dh, 0Ah, &apos;$&apos;
    ; 用 &apos;$&apos; 结尾是常见习惯(如DOS字符串)，也可以不这样做

    ; 计算字符串长度(不含 &apos;$&apos; 本身)
    count equ ($ - mess - 1)
dataseg ends

codeseg segment
assume cs:codeseg, ds:dataseg

start:
    ;------------------------------
    ; (1) 初始化数据段 DS
    ;------------------------------
    mov ax, dataseg
    mov ds, ax

    ;------------------------------
    ; (2) 准备要输出的字符串
    ;------------------------------
    mov si, offset mess  ; SI 指向 mess 首地址
    mov cx, count        ; CX=字符数量

next_char:
    ;------------------------------
    ; (3) 读 LPT 状态端口(0x379)
    ;     bit7 (0x80) 通常表示打印机忙(Busy)或空闲
    ;------------------------------
    mov dx, 379h         ; 并行口状态寄存器地址
wait_busy:
    in  al, dx           ; 读状态
    test al, 80h         ; 检查最高位(Busy位)是否为1
    je  wait_busy        ; 如果=1(忙), 就一直等

    ;------------------------------
    ; (4) 将一个字符送到 数据寄存器(0x378)
    ;------------------------------
    mov al, [si]         ; AL = mess[SI]，取当前要打印的字符
    mov dx, 378h         ; 并行口数据寄存器
    out dx, al           ; 向数据寄存器输出一个字节

    ;------------------------------
    ; (5) 设置控制寄存器(0x37A)，产生一次 strobe 脉冲
    ;    一般手法：先置 strobe=1，再置回0
    ;    0x0D = 0000 1101b
    ;    0x0C = 0000 1100b
    ;    具体bit定义因硬件而异，但普遍bit0为Strobe(取反)、bit1 = AutoFeed、bit2=Init、bit3=Select等等
    ;------------------------------
    mov dx, 37Ah         ; 并行口控制寄存器
    mov al, 0Dh          ; 0000_1101b : strobe=1, ...
    out dx, al
    mov al, 0Ch          ; 0000_1100b : strobe=0, ...
    out dx, al

    ;------------------------------
    ; (6) 准备打印下一个字符
    ;------------------------------
    inc si               ; SI++
    loop next_char       ; CX自动减1，不为0则继续

    ;------------------------------
    ; (7) 程序结束，返回DOS
    ;------------------------------
    mov ah, 4Ch
    int 21h

codeseg ends
end start

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;中断&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;在 &lt;strong&gt;8086/DOS&lt;/strong&gt; 那个时代，&lt;strong&gt;硬件端口（I/O Port）&lt;/strong&gt; 和 &lt;strong&gt;中断（Interrupt）&lt;/strong&gt; 都可能用于访问或控制同一个设备，但它们是从不同层面实现的两种机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序直接控制 I/O 端口&lt;/strong&gt;：程序可以完全按自己的节奏来访问硬件寄存器，操作非常“底层”和“直接”。不依赖操作系统或 BIOS 的服务，想怎么读写端口就怎么写。对于裸机环境（没有操作系统）或极其底层的驱动编程，需要直接操作硬件时非常方便。&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;当硬件事件发生时，CPU 会异步地中断当前程序，跳转到对应的中断服务例程 (ISR)去处理。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;键盘按下产生硬件中断 (IRQ1 → INT 09h)，ISR 会读取 0x60 端口里的扫描码，放到缓冲区；&lt;/li&gt;
&lt;li&gt;时钟芯片（8253/8254）在固定频率下产生时钟中断 (IRQ0 → INT 08h)，用来更新系统时间；&lt;/li&gt;
&lt;li&gt;硬盘/软盘控制器、网卡等也可能产生中断通知 CPU “我已经传完数据了”，ISR 去做后续处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在这些过程中，&lt;strong&gt;对端口的读写&lt;/strong&gt;依然是存在的，只不过不一定是你的主程序直接执行 &lt;code&gt;IN/OUT&lt;/code&gt;，而是&lt;strong&gt;中断服务例程&lt;/strong&gt;在适当时机去访问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;/p&gt;
&lt;p&gt;在传统的 8086/PC 架构下，&lt;strong&gt;按键输入首先会触发硬件中断&lt;/strong&gt;（IRQ1），CPU 接收到此硬件中断信号后，会跳转到键盘中断服务例程（通常对应 &lt;strong&gt;INT 09h&lt;/strong&gt;）去读取键盘端口(0x60)的数据，并保存到键盘缓冲区。应用程序往往再通过 BIOS 或操作系统提供的&lt;strong&gt;软件中断&lt;/strong&gt;（如 &lt;code&gt;INT 16h&lt;/code&gt;）读取已经保存在缓冲区里的按键信息。&lt;/p&gt;
&lt;p&gt;因此，从硬件层面看，&lt;strong&gt;键盘按下&lt;/strong&gt; = &lt;strong&gt;硬件中断&lt;/strong&gt;；
而从应用程序角度去取按键数据时，通常是&lt;strong&gt;软件中断&lt;/strong&gt;或 API 调用（比如 BIOS 的 &lt;code&gt;INT 16h&lt;/code&gt;，DOS 的 &lt;code&gt;INT 21h&lt;/code&gt; 等），但那只是读取已经由硬件中断收集到的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;中断向量表&lt;/strong&gt;：是 CPU 在执行中断时，用来“找到对应中断处理例程入口地址（CS:IP）”的一张查找表。无论是硬件中断还是软件中断，CPU 都会去这张表里取出“该中断号对应的处理函数在哪儿”，然后跳转过去执行。&lt;/p&gt;
&lt;p&gt;在 DOS 时代，操作系统为我们提供了一系列“功能号（Function Number）”，用来做文件操作、打印字符、读键盘、获取时间等等。这些功能统一通过 &lt;code&gt;INT 21h&lt;/code&gt; 这个“中断号”来调用：你把对应的功能号放进寄存器 AH，执行 &lt;code&gt;int 21h&lt;/code&gt;，DOS 内部就会去做相应的功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mov ah, 09h&lt;/code&gt; + &lt;code&gt;int 21h&lt;/code&gt; → DOS 显示字符串（直到遇到 &lt;code&gt;$&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mov ah, 4Ch&lt;/code&gt; + &lt;code&gt;int 21h&lt;/code&gt; → DOS 退出程序并返回控制给操作系统。&lt;/li&gt;
&lt;li&gt;还有很多其他，如 &lt;code&gt;AH=1&lt;/code&gt; 表示从键盘读一个字符等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中断向量表&lt;/h3&gt;
&lt;p&gt;在 8086 实模式下，内存最开始的一段区域（从物理地址 &lt;code&gt;0000:0000&lt;/code&gt; 开始）专门存放了 256 个“中断向量”——也可以理解为 256 个“指针”，每个指针占 4 个字节（前2字节是偏移 IP，后2字节是段地址 CS）。&lt;/p&gt;
&lt;p&gt;“中断号”从 0 到 255，每个号对应这表里的一条记录(&lt;code&gt;n*4&lt;/code&gt;处)。CPU 执行 &lt;code&gt;INT n&lt;/code&gt; 或响应硬件中断 n 时，就去这条记录里取出真正的处理函数入口 CS:IP。&lt;/p&gt;
&lt;p&gt;比如 &lt;strong&gt;键盘中断&lt;/strong&gt; 通常是 IRQ1，对应的是“中断号 09h (INT 9)”。按下键后，键盘控制器发出硬件信号 -&amp;gt; CPU 响应中断 09h -&amp;gt; 去中断向量表的 09h 项里，找到 BIOS/OS 对键盘的处理函数 -&amp;gt; 执行读 0x60 端口、存缓冲的过程。&lt;/p&gt;
&lt;p&gt;处理完后，用 &lt;code&gt;IRET&lt;/code&gt; 返回原程序。&lt;/p&gt;
&lt;p&gt;当指令是 &lt;code&gt;INT 21h&lt;/code&gt;，CPU 依然会去“向量表中第 21h 条”找到地址，然后跳过去。DOS 正是把“21h 这条中断向量”指向了它的内核函数入口。&lt;/p&gt;
&lt;p&gt;这样，每次执行 &lt;code&gt;int 21h&lt;/code&gt;，就能调到 DOS 做事了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例8.4	用DOS功能调用存取中断向量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOV  AL, N							; N为要挂接的中断号
MOV  AH, 35H						; DOS 功能号 35h = “获取指定中断号的中断向量”，返回的段:偏移地址存放在 ES:BX。
INT   21H                          	; 取原中断向量，返回ES:BX = 原中断服务例程地址
PUSH  ES
PUSH  BX                          	; 保存原中断向量

; 准备新中断向量指向INTHAND (本段中的例程)
PUSH  DS							; 暂存当前DS
MOV  AX, SEG  INTHAND
MOV  DS, AX    
MOV  DX, OFFSET  INTHAND

; 把中断号 N 的入口设为 DS:DX
MOV  AL, N
MOV  AH, 25H						; DOS功能：Set Interrupt Vector
INT   21H                           ; 设置新的中断向量

POP   DS
……
POP   DX
POP   DS
MOV  AL, N
MOV  AH, 25H
INT   21H                            ; 恢复原中断向量
RET
;
INTHAND:  ……
            IRET
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;示例：闹钟程序&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个中断处理程序，要求在主程序运行期间，每隔 10秒响铃一次，同时显示&lt;code&gt;‘The bell is ring!’&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;核心思路是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;挂接 INT 1Ch&lt;/strong&gt;：这是 BIOS 定时器中断“钩子”向量，每秒会被调用约 18.2 次；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在自定义中断处理程序&lt;/strong&gt;里用一个 &lt;code&gt;count&lt;/code&gt; 计数，累积到约 10 秒时就执行“响铃+打印”的动作，然后重置计数；&lt;/li&gt;
&lt;li&gt;主程序只是在空循环中“做事”，真正的周期动作全靠时钟中断来触发；&lt;/li&gt;
&lt;li&gt;退出前若需要，可恢复原 INT 1Ch 向量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面的代码尽量简化，以便更好理解。可用 MASM 或 TASM 编译为 &lt;code&gt;.EXE&lt;/code&gt;（在 DOSBox 或真实 DOS 下运行），能看到每 10 秒打印一次提示并响铃。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：由于这是挂接中断，需要“允许IRQ0”与 &lt;code&gt;sti&lt;/code&gt;。在 DOSBox 中应该默认打开时钟中断，但若有问题，可检查 &lt;code&gt;in/out 0x21&lt;/code&gt; 屏蔽位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    msg     db  &apos;The bell is ring!&apos;, 0ah, 0dh, &apos;$&apos;
    count   dw  182     ; 18.2 * 10 ≈ 182, 表示大约10秒

    oldvect dw 2 dup(?)
dataseg ends

codeseg segment
assume cs:codeseg, ds:dataseg, es:dataseg

main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    mov al, 1ch
    mov ah, 35h
    int 21h

    mov [oldvect], bx
    mov [oldvect+2], es

    mov bx, word ptr count

    mov dx, offset newIntHandler
    mov ax, seg newIntHandler
    mov ds, ax
    mov al, 1ch
    mov ah, 25h
    int 21h

    mov ax, dataseg
    mov ds, ax

    sti

main_loop:
    nop
    jmp main_loop

    pop ax
    pop ds
    ret

main endp

ring proc near
    push dx
    push cx
    push ax
    
    push ds

    mov ax, dataseg
    mov ds, ax

    ; --- 发声循环开始 ---
    mov dx, 100h         ; 控制循环次数
    in  al, 61h          ; 读 0x61 端口
    and al, 11111101b    ; 先清除 bit0/bit1，关闭喇叭
    out 61h, al

    sound_loop:
        xor al, 00000010b    ; 翻转 bit1 (产生方波)
        out 61h, al
        mov cx, 140h         ; 延时，影响音调
    delay1:
        loop delay1

    dec dx
    jne sound_loop
    ; --- 发声循环结束 ---

    ; 退出前关掉喇叭
    in  al, 61h
    and al, 11111101b
    out 61h, al


    pop ds
    pop ax
    pop cx
    pop dx
    ret
ring endp

newIntHandler proc near

    dec bx
    jnz skip

    call ring
    lea dx, msg
    mov ah, 09h
    int 21h
    mov bx, word ptr count

    skip:

    iret
newIntHandler endp

codeseg ends
end main

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>从零开始的汇编语言笔记（2）</title><link>https://fuwari.vercel.app/posts/course/assembly/assembly-note-2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/assembly/assembly-note-2/</guid><pubDate>Thu, 02 Jan 2025 15:46:06 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这里应该是一些主要的代码模板记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;一. 基础读写&lt;/h2&gt;
&lt;h3&gt;1. 单个非负整数输入&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;控制台输入一个十进制整数字符串，转成数存储。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment

    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)

    input_num           dw ?
    
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax

    call read_num
    mov ax, input_num

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp

; 输入一个整数，存入input_num
read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    read_loop:
        mul bx
        mov dl, [si]
        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    mov input_num, ax

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 单个非负整数输出&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    lea si, output_buffer
    mov ax, output_num

    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

        call newline

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合并后的输入输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment

    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)

    input_num           dw ?

    output_num      dw  ?
    output_buffer   db  10 dup (?)
    
dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc far
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    call read_num
    mov ax, input_num

    mov output_num, ax
    call write_num

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

main endp

; 输入一个整数，存入input_num
read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    read_loop:
        mul bx
        mov dl, [si]
        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    mov input_num, ax

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    lea si, output_buffer
    mov ax, output_num

    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

        call newline

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 多个非负数输入输出&lt;/h3&gt;
&lt;h4&gt;（1） 每行一个&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;每行输入1个数。&lt;/p&gt;
&lt;p&gt;输入结束后输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)
    input_num           dw ?

    output_num      dw  ?
    output_buffer   db  10 dup (?)

    nums_len    dw  3
    nums        dw  3 dup (?)

dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    mov cx, [nums_len]
    lea si, nums
    get_nums_loop:
        call read_num
        mov ax, [input_num]
        mov word ptr [si], ax
        add si, 2
        loop get_nums_loop

;    call sort_num

    mov cx, [nums_len]
    lea si, nums
    put_nums_loop:
        mov ax, [si]
        mov [output_num], ax
        call write_num
        add si, 2
        loop put_nums_loop

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp


; 输入一个整数，存入input_num
read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, [input_buffer_len]
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    read_loop:
        mul bx
        mov dl, [si]
        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    mov [input_num], ax

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    lea si, output_buffer
    mov ax, [output_num]

    xor cx, cx              ; 位数计数器
    mov bx, 10
    write_loop:
        xor dx, dx
        div bx              ; AX = AX / 10, 商在 AX, 余数在 DX
        or dl, 030h         ; 转换为ASCII码
        mov [si], dl        ; 存储字符
        inc si              ; 指向下一个位置
        inc cx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    dec si                  ; 指向最后一个
    lea di, output_buffer   ; 指向第一个
    shr cx, 1               
    jz write_num_print      ; （防止cx=0时的循环）

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

        call newline

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2） 每行多个&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;读入一行数，存入&lt;code&gt;input_nums&lt;/code&gt;中，并且数的个数存入&lt;code&gt;input_cnt&lt;/code&gt;中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_nums          dw 20 dup(?)
    input_cnt           dw ?

    input_invalid       db 0

    input_buffer        db 100
    input_buffer_len    db ?
    input_buffer_data   db 100 dup(?)


    output_num      dw  ?
    output_buffer   db  10 dup (?)

    nums_len    dw  ?
    nums        dw  20 dup (?)

dataseg ends

stackseg segment
    db 100 dup (?)
stackseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg, ss:stackseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax
    mov ax, stackseg
    mov ss, ax

    call readline_num

    call writeline_num

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

; 输入一行多个整数，存入
readline_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len    
    lea si, input_buffer_data   ; 扫描到的原始字符串位置

    mov ax, 0                   ; 存储当前结果
    lea di, input_nums          ; 当前正在写入的位置
    mov dl, 0                   ; 标志位，最低位表示是否有数尚未存入结果
                                ;        第二位表示是否有负数标志

    xor bh, bh

    readline_loop:
        mov bl, byte ptr [si]

        cmp bl, &apos; &apos;             
        je readline_isspace     ; 是空格
        cmp bl, 0Dh             ; 回车
        je readline_isspace 
        cmp bl, 0Ah             ; 换行
        je readline_isspace 
        cmp bl, &apos;-&apos;
        je readline_isneg

        cmp bl, &apos;0&apos;             ; 检查是否是数字
        jb readline_invalid
        cmp bl, &apos;9&apos;
        ja readline_invalid
        jmp readline_isnumber

        readline_isneg:
            test dl, 02h            ; 多个符号
            jnz readline_invalid
            xor dl, 02h             ; 标志位
            jmp readline_charover

        readline_isspace:
            test dl, 03h            ; 完全没开始
            jz readline_charover
            and dl, 03h
            cmp dl, 02h             ; 检查禁止的情况，仅有一个符号
            je readline_invalid     
            test dl, 02h            ; 是负数
            jz readline_storenum
            neg ax
            
            readline_storenum:
            mov word ptr [di], ax   ; 存储
            xor ax, ax              ; 清空
            add di, 2               ; 进位

            mov dl, 0               ; 标志位清空
            jmp readline_charover

        readline_isnumber:
            or dl, 1                ; 标志位
            and bl, 0Fh             ; ASCII转成数
            push dx
            mov dx, 10
            mul dx                  ; *10
            pop dx
            add ax, bx              ; 加当前位
        
        readline_charover:
            inc si
        loop readline_loop
    
    ; 检查是否有数没有存储
    test dl, 01h
    jz readline_ok

    ; 处理末尾
    mov word ptr [di], ax
    add di, 2

    jmp readline_ok
    
    readline_invalid:   ; 非法输入
        mov input_invalid, 1

    readline_ok:

    lea si, input_nums
    sub di, si
    sar di, 1

    mov input_cnt, di   ; 写入个数

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
readline_num endp

; 输出一行多个整数
writeline_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    lea si, input_nums   ; 位置
    mov cx, input_cnt   ; 长度

    writeline_loop:
        mov ax, word ptr [si]
        mov output_num, ax

        call write_num

        mov ah, 02h
        mov dl, &apos; &apos;
        int 21h

        add si, 2
        loop writeline_loop

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
writeline_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    lea si, output_buffer
    mov ax, output_num

    ; 负数处理
    cmp ax, 0
    jge direct_write_num
    push ax
    mov ah, 02h
    mov dl, &apos;-&apos;
    int 21h
    pop ax
    neg ax
    direct_write_num:
    
    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

    ;    call newline   取决于要不要换行

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 常见任务&lt;/h2&gt;
&lt;h3&gt;1. 排序&lt;/h3&gt;
&lt;p&gt;一个简单的选择排序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)
    input_num           dw ?

    output_num      dw  ?
    output_buffer   db  10 dup (?)

    nums_len    dw  ?
    nums        dw  20 dup (?)

    prompt_input_num    db  &apos;Input the number of numbers: &apos;, &apos;$&apos;
    prompt_input_num_d  db  &apos;Input the number: &apos;, &apos;$&apos;

dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    mov ah, 09h
    lea dx, prompt_input_num
    int 21h
    call read_num

    mov ah, 09h
    lea dx, prompt_input_num_d
    int 21h
    
    mov cx, input_num
    mov nums_len, cx
    lea si, nums

    get_nums_loop:
        call read_num
        mov ax, input_num
        mov word ptr [si], ax
        add si, 2
        loop get_nums_loop

    call sort_num

    mov cx, nums_len
    lea si, nums
    put_nums_loop:
        mov ax, word ptr [si]
        mov output_num, ax
        call write_num
        add si, 2
        loop put_nums_loop

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

sort_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di
    
    mov si, [nums_len]
    sal si, 1
    lea bx, [nums + si]     ; 截止位
    lea si, nums

    sort_loop:

        lea di, [si + 2]
        cmp di, bx
        je scan_over

        scan_loop:
            mov ax, word ptr [di]
            cmp word ptr [si], ax
            jna scan_loop_over
            ; 开始交换
            mov     ax, word ptr [si]
            xchg    ax, word ptr [di]
            xchg    ax, word ptr [si]
            scan_loop_over:
            add di, 2
            cmp di, bx
            jne scan_loop

        scan_over:
        add si, 2
        cmp si, bx
        jne sort_loop

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax

    ret
sort_num endp

; 输入一个整数，存入input_num
read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    read_loop:
        mul bx
        mov dl, [si]
        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    mov input_num, ax

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    lea si, output_buffer
    mov ax, output_num

    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

        call newline

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241126221839252.png&quot; alt=&quot;image-20241126221839252&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;2. 进制转换&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;输入一个十进制数，转成十六进制输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;*报错&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;operand must have size&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mov [si], &apos;$&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该修改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov byte ptr [si], &apos;$&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;Expected: comma&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;shr cx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有指定位移的位数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shr cx, 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;往年上机题&lt;/h2&gt;
&lt;h3&gt;2021级：输入一组数，输出中位数&lt;/h3&gt;
&lt;p&gt;也就是前面排序的类似的，取出中间的数即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)
    input_num           dw ?

    output_num      dw  ?
    output_buffer   db  10 dup (?)

    nums_len    dw  ?
    nums        dw  20 dup (?)

    prompt_input_num    db  &apos;Input the number of numbers: &apos;, &apos;$&apos;
    prompt_input_num_d  db  &apos;Input the number: &apos;, &apos;$&apos;
    prompt_mid          db  &apos;The middle is : &apos;, &apos;$&apos;

dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    mov ah, 09h
    lea dx, prompt_input_num
    int 21h
    call read_num

    mov ah, 09h
    lea dx, prompt_input_num_d
    int 21h
    
    mov cx, input_num
    mov nums_len, cx
    lea si, nums

    get_nums_loop:
        call read_num
        mov ax, input_num
        mov word ptr [si], ax
        add si, 2
        loop get_nums_loop

    call sort_num


    mov ah, 09h
    lea dx, prompt_mid
    int 21h

    mov cx, nums_len
    inc cx
    shr cx, 1   ; (n+1)/2
    dec cx      ; 调整为零基
    shl cx, 1   ; 2字节一个字

    lea si, nums
    add si, cx

    mov ax, word ptr [si]
    mov output_num, ax
    call write_num


    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

sort_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di
    
    mov si, [nums_len]
    sal si, 1
    lea bx, [nums + si]     ; 截止位
    lea si, nums

    sort_loop:

        lea di, [si + 2]
        cmp di, bx
        je scan_over

        scan_loop:
            mov ax, word ptr [di]
            cmp word ptr [si], ax
            jna scan_loop_over
            ; 开始交换
            mov     ax, word ptr [si]
            xchg    ax, word ptr [di]
            xchg    ax, word ptr [si]
            scan_loop_over:
            add di, 2
            cmp di, bx
            jne scan_loop

        scan_over:
        add si, 2
        cmp si, bx
        jne sort_loop

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax

    ret
sort_num endp

; 输入一个整数，存入input_num
read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    read_loop:
        mul bx
        mov dl, [si]
        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    mov input_num, ax

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si

    lea si, output_buffer
    mov ax, output_num

    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

        call newline

    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2021级：输入字符串，改成小写后按字母表输出&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&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;按a-z的顺序输出出现过的字母&lt;/p&gt;
&lt;p&gt;如果字母没有出现就用空格代替，不能重复输出相同字母&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;（大概从0开始写了20min）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup (?)

    answer_buffer       db 26 dup (&apos; &apos;), &apos;$&apos;

dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    mov ah, 0ah
    lea dx, input_buffer
    int 21h

    call newline

    xor ch, ch
    mov cl, input_buffer_len
    lea si, input_buffer_data

    mov bl, byte ptr &apos;a&apos;
    sub bl, byte ptr &apos;A&apos;

    ; 改成小写
    search_loop:
        mov al, byte ptr [si]
        cmp al, byte ptr &apos;A&apos;
        jb no_match
        cmp al, byte ptr &apos;Z&apos;
        ja no_match

        add al, bl
        mov byte ptr [si], al

        no_match:

        inc si
        loop search_loop
    
    mov byte ptr [si], &apos;$&apos;

    ; mov ah, 09h
    ; lea dx, input_buffer_data
    ; int 21h

    ; call newline

    xor ch, ch
    mov cl, input_buffer_len
    lea si, input_buffer_data

    solve_loop:
        mov al, byte ptr [si]

        cmp al, byte ptr &apos;a&apos;
        jb solve_loop_over
        cmp al, byte ptr &apos;z&apos;
        ja solve_loop_over

        mov bl, al
        sub bl, byte ptr &apos;a&apos;

        xor bh, bh
        lea di, answer_buffer
        add di, bx

        mov [di], byte ptr &apos;a&apos;
        add [di], bl

        solve_loop_over:
        inc si
        loop solve_loop
    
    mov ah, 09h
    lea dx, answer_buffer
    int 21h

    call newline

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

newline proc near
    push ax
    push ds
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2020级：接受十进制数输入朝代，将朝代转成对应年份输出&lt;/h3&gt;
&lt;blockquote&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;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_buffer        db 20
    input_buffer_len    db ?
    input_buffer_data   db 20 dup(?)
    input_num           dw ?

    input_prompt        db &apos;Input Year: &apos;, &apos;$&apos;

    year_cnt dw 7
    year_ans dw -10000, -500, 800, 1000, 1300, 1500, 1700, 10000
    dyna_ans db &apos;Qin $Han $Tang$Song$Yuan$Ming$Qing$&apos;

    invalid_tag         db 0

dataseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

main_loop:
    mov ah, 09h
    lea dx, input_prompt
    int 21h

    call read_num
    mov al, invalid_tag
    cmp al, 1
    
    je main_over
    call answer
    jmp main_loop

main_over:
    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

answer proc near
    mov cx, year_cnt
    lea si, year_ans
    lea di, dyna_ans

    mov dx, input_num
    answer_loop:
        mov ax, [si]
        mov bx, [si+2]
        cmp dx, ax
        jl no_answer
        cmp dx, bx
        jge no_answer
        jmp answer_find
        no_answer:
        add si, 2
        add di, 5
        loop answer_loop

    answer_find:
    mov ah, 09h
    mov dx, di
    int 21h

    call newline

    ret
answer endp

read_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline

    xor ch, ch
    mov cl, [input_buffer_len]
    mov ax, 0
    lea si, input_buffer_data
    mov bx, 10
    xor dx, dx

    mov dl, [si]
    cmp dl, &apos;-&apos;
    jne read_loop
    inc si
    dec cx

    read_loop:
        mul bx
        mov dl, [si]

        cmp dl, &apos;0&apos;
        jb read_invalid
        cmp dl, &apos;9&apos;
        ja read_invalid

        and dl, 0Fh
        add ax, dx
        inc si
        loop read_loop

    lea si, input_buffer_data
    mov dl, [si]
    cmp dl, &apos;-&apos;
    jne read_store
    neg ax
    read_store:
    mov [input_num], ax

    jmp read_num_over

    read_invalid:
    mov ax, 1
    mov invalid_tag, al
    read_num_over:
    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
read_num endp

newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2020级：加减法表达式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    input_nums          dw 20 dup(?)
    input_cnt           dw ?

    input_invalid       db 0

    input_buffer        db 100
    input_buffer_len    db ?
    input_buffer_data   db 100 dup(?)

    fuhao       db ?

    output_num      dw  ?
    output_buffer   db  10 dup (?)

    nums_len    dw  ?
    nums        dw  20 dup (?)

dataseg ends

stackseg segment
    db 100 dup (?)
stackseg ends

codeseg segment
    assume cs:codeseg, ds:dataseg, es:dataseg, ss:stackseg
main proc near
    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax
    mov ax, stackseg
    mov ss, ax

    call readline_num

    mov ax, [input_nums]
    mov bx, [input_nums+2]

    mov cl, fuhao
    cmp cl, &apos;+&apos;
    je adddd
    jne deccc

adddd:
    add ax, bx
    jmp main_over
deccc:
    sub ax, bx
main_over:
    mov input_nums, ax
    mov ax, 1
    mov input_cnt, ax
    call writeline_num

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h
main endp

; 输入一行多个整数，存入
readline_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    mov ah, 0Ah
    lea dx, input_buffer
    int 21h

    call newline
    
    xor ch, ch
    mov cl, input_buffer_len    
    lea si, input_buffer_data   ; 扫描到的原始字符串位置

    mov ax, 0                   ; 存储当前结果
    lea di, input_nums          ; 当前正在写入的位置
    mov dl, 0                   ; 标志位，最低位表示是否有数尚未存入结果
                                ;        第二位表示是否有负数标志

    xor bh, bh

    readline_loop:
        mov bl, byte ptr [si]

        cmp bl, &apos;-&apos;
        je readline_isspace
        cmp bl, &apos;+&apos;
        je readline_isspace


        cmp bl, &apos;0&apos;             ; 检查是否是数字
        jb readline_invalid
        cmp bl, &apos;9&apos;
        ja readline_invalid
        jmp readline_isnumber


        readline_isspace:
            test dl, 03h            ; 完全没开始
            jz readline_charover
            and dl, 03h
            cmp dl, 02h             ; 检查禁止的情况，仅有一个符号
            je readline_invalid     
            test dl, 02h            ; 是负数
            jz readline_storenum
            neg ax
            
            readline_storenum:
            mov word ptr [di], ax   ; 存储
            xor ax, ax              ; 清空
            add di, 2               ; 进位
            mov fuhao, bl

            mov dl, 0               ; 标志位清空
            jmp readline_charover

        readline_isnumber:
            or dl, 1                ; 标志位
            and bl, 0Fh             ; ASCII转成数
            push dx
            mov dx, 10
            mul dx                  ; *10
            pop dx
            add ax, bx              ; 加当前位
        
        readline_charover:
            inc si
        loop readline_loop
    
    ; 检查是否有数没有存储
    test dl, 01h
    jz readline_ok

    ; 处理末尾
    mov word ptr [di], ax
    add di, 2

    jmp readline_ok
    
    readline_invalid:   ; 非法输入
        mov input_invalid, 1

    readline_ok:

    lea si, input_nums
    sub di, si
    sar di, 1

    mov input_cnt, di   ; 写入个数

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
readline_num endp

; 输出一行多个整数
writeline_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    lea si, input_nums   ; 位置
    mov cx, input_cnt   ; 长度

    writeline_loop:
        mov ax, word ptr [si]
        mov output_num, ax

        call write_num

        mov ah, 02h
        mov dl, &apos; &apos;
        int 21h

        add si, 2
        loop writeline_loop

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
writeline_num endp

; 输出一个整数，存在output_num
write_num proc near
    push ax
    push bx
    push cx
    push dx
    push si
    push di

    lea si, output_buffer
    mov ax, output_num

    ; 负数处理
    cmp ax, 0
    jge direct_write_num
    push ax
    mov ah, 02h
    mov dl, &apos;-&apos;
    int 21h
    pop ax
    neg ax
    direct_write_num:
    
    xor dx, dx
    mov cx, 10
    write_loop:
        div cx              ; AX = AX / 10, 商在 AX, 余数在 DX
        mov bx, dx          ; BL = 余数
        or bl, 030h         ; 转换为 ASCII 字符
        mov [si], bl        ; 存储字符
        inc si              ; 指向下一个位置
        xor dx, dx

        cmp ax, 0
        jne write_loop
    
    mov byte ptr [si], &apos;$&apos;
    
    lea di, output_buffer
    mov cx, si
    sub cx, di
    dec si
    shr cx, 1
    jz write_num_print

    reverse_loop:
        mov al, [si]
        xchg al, [di]
        xchg al, [si]
        inc di
        dec si
        loop reverse_loop
    
    write_num_print:
        mov ah, 09h
        lea dx, output_buffer
        int 21h

    ;    call newline   取决于要不要换行

    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax
    ret
write_num endp

; 换行
newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

codeseg ends

end main

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>从零开始的汇编语言笔记（3）</title><link>https://fuwari.vercel.app/posts/course/assembly/assembly-note-3/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/assembly/assembly-note-3/</guid><pubDate>Thu, 02 Jan 2025 15:46:05 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这里应该就是一些比较大的代码记录，其实就是课本题品鉴一下自己写一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[6.1]   编写一个&lt;strong&gt;子程序嵌套结构&lt;/strong&gt;的程序模块，分别从键盘输入姓名及8个字符的电话号码，并以一定的格式显示出来。&lt;/p&gt;
&lt;p&gt;主程序&lt;code&gt;TELIST&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;·   显示提示符&lt;code&gt;INPUT NAME：&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;·   调用子程序&lt;code&gt;INPUT_NAME&lt;/code&gt;输入姓名；&lt;/p&gt;
&lt;p&gt;·   显示提示符&lt;code&gt;INPUT A TELEPHONE NUMBER：&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;·   调用子程序&lt;code&gt;INPHONE&lt;/code&gt;输入电话号码；&lt;/p&gt;
&lt;p&gt;·   调用子程序&lt;code&gt;PRINTLINE&lt;/code&gt;显示姓名及电话号码。&lt;/p&gt;
&lt;p&gt;子程序&lt;code&gt;INPUT_NAME&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;·   调用键盘输入子程序&lt;code&gt;GETCHAR&lt;/code&gt;，把输入的姓名存放在&lt;code&gt;INBUF&lt;/code&gt;缓冲区中；&lt;/p&gt;
&lt;p&gt;·   把&lt;code&gt;INBUF&lt;/code&gt;中的姓名移入输出行&lt;code&gt;OUTNAME&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;子程序&lt;code&gt;INPHONE&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;·   调用键盘输入子程序&lt;code&gt;GETCHAR&lt;/code&gt;，把输入的8位电话号码存放在&lt;code&gt;INBUF&lt;/code&gt;缓冲区中；&lt;/p&gt;
&lt;p&gt;·   把&lt;code&gt;INBUF&lt;/code&gt;中的号码移入输出行&lt;code&gt;OUTPHONE&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;子程序&lt;code&gt;PRINTLINE&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;显示姓名及电话号码，格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME    TEL.
X X X        XXXXXXXX
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是课本答案的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;;******************************************
; 数据段定义
DSEG        SEGMENT
    INBUF    DB  12 DUP (&apos; &apos;)            ; 输入缓冲区，初始化为空格，长度12个字节
    OUTNAME  DB  16 DUP (&apos; &apos;)            ; 姓名输出行，初始化为空格，长度16个字节
    OUTPHONE DB  12 DUP (&apos; &apos;), 0DH, 0AH, &apos;$&apos; ; 电话号码输出行，初始化为空格，后跟回车换行符和字符串结束符
    MESG1    DB  &apos;INPUT  NAME：&apos;, &apos;$&apos;    ; 提示输入姓名的字符串
    MESG2    DB  &apos;INPUT  A  TELEPHONE  NUMBER：&apos;, &apos;$&apos; ; 提示输入电话号码的字符串
    MESG3    DB  &apos;NAME&apos;, 12 DUP (&apos; &apos;), &apos;TEL.&apos;, 0DH, 0AH, &apos;$&apos; ; 显示标题行的字符串
    CRLF     DB  0DH, 0AH, &apos;$&apos;          ; 回车换行符字符串
DSEG        ENDS                        ; 数据段结束
;******************************************

;******************************************
; 堆栈段定义
STACK       SEGMENT
    DW  100 DUP (?)                    ; 堆栈空间，100个字（200字节），未初始化
    TOS     LABEL  WORD                 ; 堆栈顶（Top of Stack）的标签
STACK       ENDS                        ; 堆栈段结束
;******************************************

;******************************************
; 代码段定义
CSEG        SEGMENT
; 主程序TELIST，远过程（FAR PROC）
TELIST      PROC    FAR
    ASSUME  CS: CSEG, DS: DSEG, ES: DSEG, SS: STACK ; 假设段寄存器的关联关系

; 初始化堆栈段寄存器SS和堆栈指针SP
START:      MOV     AX, STACK            ; 将堆栈段的段地址加载到AX
             MOV     SS, AX               ; 将AX的值（STACK段地址）赋给SS寄存器
             MOV     SP, OFFSET TOS        ; 将堆栈顶的偏移地址赋给SP寄存器

; 保存原有的数据段寄存器DS
             PUSH    DS                    ; 将DS压入堆栈，保存原始DS
             SUB     AX, AX                ; 将AX清零
             PUSH    AX                    ; 将AX（现在为0）压入堆栈，可能用于设置段寄存器
             MOV     AX, DSEG              ; 将数据段地址加载到AX
             MOV     DS, AX                ; 设置DS寄存器为DSEG
             MOV     ES, AX                ; 设置ES寄存器为DSEG

; 显示提示符“INPUT NAME：”
BEGIN:      LEA     DX, MESG1             ; 将MESG1的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示字符串
             
             CALL    INPUT_NAME            ; 调用子程序INPUT_NAME，输入姓名

; 显示提示符“INPUT A TELEPHONE NUMBER：”
             LEA     DX, MESG2             ; 将MESG2的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示字符串
             
             CALL    INPHONE               ; 调用子程序INPHONE，输入电话号码

             CALL    PRINTLINE             ; 调用子程序PRINTLINE，显示姓名及电话号码

             RET                            ; 远返回，结束主程序
TELIST      ENDP                          ; 主程序结束
;--------------------------------------------------------------------------

; 子程序INPUT_NAME，近过程（NEAR PROC）
INPUT_NAME  PROC    NEAR
             CALL    GETCHAR               ; 调用子程序GETCHAR，输入姓名
             
             LEA     SI, INBUF             ; 将INBUF的地址加载到SI（源索引寄存器）
             LEA     DI, OUTNAME           ; 将OUTNAME的地址加载到DI（目的索引寄存器）
             MOV     CX, 12                ; 设置循环计数器CX为12
             CLD                            ; 清除方向标志，字符串操作向前移动
             REP     MOVSB                 ; 重复移动字节，将INBUF的数据复制到OUTNAME
             
             RET                            ; 返回
INPUT_NAME  ENDP                          ; INPUT_NAME子程序结束
;--------------------------------------------------------------------------

; 子程序INPHONE，近过程（NEAR PROC）
INPHONE     PROC    NEAR
             CALL    GETCHAR               ; 调用子程序GETCHAR，输入电话号码
             
             LEA     SI, INBUF             ; 将INBUF的地址加载到SI（源索引寄存器）
             LEA     DI, OUTPHONE          ; 将OUTPHONE的地址加载到DI（目的索引寄存器）
             MOV     CX, 12                ; 设置循环计数器CX为12
             CLD                            ; 清除方向标志，字符串操作向前移动
             REP     MOVSB                 ; 重复移动字节，将INBUF的数据复制到OUTPHONE
             
             RET                            ; 返回
INPHONE     ENDP                          ; INPHONE子程序结束
;--------------------------------------------------------------------------

; 子程序GETCHAR，近过程（NEAR PROC）
GETCHAR     PROC    NEAR
             MOV     AL, 20H               ; 将空格字符（20H）加载到AL
             MOV     CX, 12                ; 设置循环计数器CX为12
             LEA     DI, INBUF             ; 将INBUF的地址加载到DI
             CLD                            ; 清除方向标志，字符串操作向前移动
             REP     STOSB                 ; 重复存储字节，将20H填充到INBUF
             
             MOV     CX, 12                ; 设置循环计数器CX为12，用于输入字符
             MOV     DI, 0                 ; 将DI清零，用于索引INBUF
INPUT:       MOV     AH, 1                 ; 设置AH为1，DOS读取字符功能
             INT     21H                   ; 调用DOS中断，读取一个字符
             
             CMP     AL, 0DH               ; 比较输入字符是否为回车符（0DH）
             JZ      QUIT                  ; 如果是回车符，跳转到QUIT
             
             MOV     [INBUF + DI], AL      ; 将输入的字符存入INBUF[DI]
             INC     DI                    ; 增加DI，指向下一个位置
             LOOP    INPUT                 ; 循环，直到CX为0
QUIT:        CALL    DISP_CRLF             ; 调用子程序DISP_CRLF，显示回车换行
             RET                            ; 返回
GETCHAR     ENDP                          ; GETCHAR子程序结束
;--------------------------------------------------------------------------

; 子程序PRINTLINE，近过程（NEAR PROC）
PRINTLINE   PROC    NEAR
             LEA     DX, MESG3             ; 将MESG3的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示字符串
             
             LEA     DX, OUTNAME           ; 将OUTNAME的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示姓名
             
             LEA     DX, OUTPHONE          ; 将OUTPHONE的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示电话号码
             
             RET                            ; 返回
PRINTLINE   ENDP                          ; PRINTLINE子程序结束
;--------------------------------------------------------------------------

; 子程序DISP_CRLF，近过程（NEAR PROC）
DISP_CRLF   PROC    NEAR
             LEA     DX, CRLF              ; 将CRLF的地址加载到DX
             MOV     AH, 09H               ; 设置AH为09H，DOS显示字符串功能
             INT     21H                   ; 调用DOS中断，显示回车换行
             RET                            ; 返回
DISP_CRLF   ENDP                          ; DISP_CRLF子程序结束
;--------------------------------------------------------------------------

CSEG        ENDS                            ; 代码段结束
;******************************************

END     START                               ; 程序入口点

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是我的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dataseg segment
    prompt_name     db &apos;INPUT NAME: &apos;, &apos;$&apos;
    prompt_phone    db &apos;INPUT A TELEPHONE NUMBER: &apos;, &apos;$&apos;

    INBUF           db 20
    INBUF_LEN       db ?
    INBUF_DATA      db 20 dup(?)

    OUTNAME         db 20 dup(?)
    OUTPHONE        db 20 dup(?)

    prompt_title    db  &apos;NAME&apos;, 8 DUP (&apos; &apos;), &apos;TEL.&apos;, 0DH, 0AH, &apos;$&apos;
dataseg ends

stackseg segment
    dw  100 dup(?)
    TOS label word
stackseg ends

codeseg segment
    assume cs: codeseg, ds: dataseg, ss: stackseg
TELIST proc far
start:
    mov ax, stackseg
    mov ss, ax
    mov sp, offset TOS

    push ds
    xor ax, ax
    push ax
    mov ax, dataseg
    mov ds, ax
    mov es, ax

    call INPUT_NAME
    call INPHONE

    call PRINTLINE

    pop ax
    pop ds
    mov ax, 4C00h
    int 21h

TELIST endp

INPUT_NAME proc near
    mov ah, 09h
    lea dx, prompt_name
    int 21h

    call GETCHAR
    mov cx, 0

    lea si, INBUF_DATA
    lea di, OUTNAME
    mov cl, INBUF_LEN
    cld
    rep movsb

    mov cl, 12
    sub cl, INBUF_LEN
    mov al, &apos; &apos;
    rep stosb

    mov byte ptr [di], &apos;$&apos;

    ret
INPUT_NAME endp

INPHONE proc near
    mov ah, 09h
    lea dx, prompt_phone
    int 21h

    call GETCHAR

    lea si, INBUF_DATA
    lea di, OUTPHONE
    mov cl, INBUF_LEN
    cld
    rep movsb

    mov cl, 12
    sub cl, INBUF_LEN
    mov al, &apos; &apos;
    rep stosb
    
    mov byte ptr [di], &apos;$&apos;
    ret
INPHONE endp

GETCHAR proc near
    mov ah, 0Ah
    lea dx, INBUF
    int 21h
    call newline
    ret
GETCHAR endp

newline proc near
    push ax
    push dx
    mov ah, 02h

    mov dl, 0Dh
    int 21h
    mov dl, 0Ah
    int 21h

    pop dx
    pop ax
    ret
newline endp

PRINTLINE proc near
    mov ah, 09h
    lea dx, prompt_title
    int 21h

    mov ah, 09h
    lea dx, OUTNAME
    int 21h

    mov ah, 09h
    lea dx, OUTPHONE
    int 21h

    ret
PRINTLINE endp
codeseg ends
end start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241202062433154.png&quot; alt=&quot;image-20241202062433154&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>从零开始的汇编语言笔记（4）</title><link>https://fuwari.vercel.app/posts/course/assembly/assembly-note-4/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/course/assembly/assembly-note-4/</guid><pubDate>Thu, 02 Jan 2025 15:46:04 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这里应该就是一些理论的笔记部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h1&gt;计算机理论&lt;/h1&gt;
&lt;h2&gt;小端模式&lt;/h2&gt;
&lt;p&gt;下面证明一下“低位对低位，高位对高位”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\&amp;gt; debug
- a
073F:0100	mov ax, 1234
073F:0103	mov [6666], ax
073F:0106	int 3			; 设置断点
073F:0107					; 回车退出
- g							; 运行到断点
- d ds:6666

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后可以看见这个&lt;code&gt;ds:6666&lt;/code&gt;位置的数，是地址低位是&lt;code&gt;34&lt;/code&gt;（数值低位），地址高位是&lt;code&gt;12&lt;/code&gt;（数值高位）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241227150838997.png&quot; alt=&quot;image-20241227150838997&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;外设接口&lt;/h2&gt;
&lt;p&gt;外部设备与主机（CPU和z存储器）的通信是通过外设接口（Interface）进行的，每个接口包括一组寄存器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数据寄存器：存放外设和主机间传送的数据。&lt;/p&gt;
&lt;p&gt;状态寄存器：保存外设或接口的状态信息。&lt;/p&gt;
&lt;p&gt;命令寄存器：保存CPU发给外设或接口的控制命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;外设中每个寄存器有一个端口（Port）地址，构成一个独立于内存的I/O地址空间：&lt;code&gt;0000H &lt;/code&gt;~ &lt;code&gt;0FFFFH&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;专用的I/O指令： &lt;code&gt;IN&lt;/code&gt;和&lt;code&gt;OUT&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;BIOS和DOS功能调用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;CPU和外设（比如键盘、打印机、硬盘等）的工作速度差异很大，而且它们&quot;说的语言&quot;也不同。接口就像是一个翻译和缓冲区，帮助它们能够顺利通信。&lt;/p&gt;
&lt;p&gt;接口中的三种关键寄存器各自担任不同的角色：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据寄存器： 这就像是一个传递文件的托盘。当键盘输入一个字符时，这个字符会先放在数据寄存器中，然后CPU才能来取走它。反过来，当CPU要向打印机发送数据时，也是先把数据放在这个&quot;托盘&quot;中。&lt;/li&gt;
&lt;li&gt;状态寄存器： 这像是一个状态指示牌，告诉CPU外设当前的工作状态。比如，打印机是否缺纸、键盘是否有按键按下、数据是否准备好等。CPU通过查看状态寄存器，就知道外设是否准备好进行数据传输。&lt;/li&gt;
&lt;li&gt;命令寄存器： 这相当于一个指令箱，CPU通过它发送控制命令给外设。比如，让打印机开始打印、让硬盘开始读取数据等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;端口地址空间（&lt;code&gt;0000H&lt;/code&gt;~&lt;code&gt;0FFFFH&lt;/code&gt;）是一个很巧妙的设计： 这就像是给每个接口寄存器都分配了一个门牌号。这些地址与内存地址是分开的，形成了一个独立的地址空间。这样，CPU就可以明确知道它是在与内存通信还是在与外设通信。&lt;/p&gt;
&lt;p&gt;为了使用这些端口，处理器提供了专门的指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IN &lt;/code&gt;指令：从指定端口读取数据（就像从外设收信）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OUT&lt;/code&gt; 指令：向指定端口写入数据（就像给外设发信）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个具体的例子： 假设键盘的数据端口地址是&lt;code&gt;60H&lt;/code&gt;，状态端口是&lt;code&gt;64H&lt;/code&gt;。当我们按下一个键时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU通过&lt;code&gt;IN&lt;/code&gt;指令从&lt;code&gt;64H&lt;/code&gt;端口读取状态，检查是否有新数据&lt;/li&gt;
&lt;li&gt;如果有新数据，就通过&lt;code&gt;IN&lt;/code&gt;指令从&lt;code&gt;60H&lt;/code&gt;端口读取按键数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（下面代码抄的， 没跑过）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 从键盘读取数据的简单例子
mov dx, 60h        ; 60h是键盘数据端口
in al, dx          ; 从端口读取一个字节到AL寄存器

; 向打印机发送数据的例子
mov dx, 378h       ; 378h是打印机数据端口
mov al, &apos;A&apos;        ; 要打印的字符
out dx, al         ; 发送字符到打印机端口
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;; 完整的键盘读取程序
check_keyboard:
    mov dx, 64h    ; 键盘状态端口
wait_key:
    in al, dx      ; 读取键盘状态
    test al, 1     ; 测试是否有数据可读（第0位）
    jz wait_key    ; 如果没有数据，继续等待

    mov dx, 60h    ; 键盘数据端口
    in al, dx      ; 读取按键扫描码
    
    ; 此时AL中包含了按键的扫描码
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;实模式存储器寻址&lt;/h2&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241227203922890.png&quot; alt=&quot;image-20241227203922890&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;寻址方式&lt;/h2&gt;
&lt;h3&gt;立即寻址&lt;/h3&gt;
&lt;p&gt;（立即数寻址）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov al, 5
mov ax, 3064h
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;寄存器寻址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mov ax, bx
mov al, bh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;直接寻址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mov ax, [2000h]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;寄存器间接寻址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mov ax, [bx]
mov ax, es:[bx]
mov ax, [bp]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;寄存器相对寻址&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241227204632197.png&quot; alt=&quot;image-20241227204632197&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov ax, count[si]
mov ax, [count+si]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基址变址寻址&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241227204659985.png&quot; alt=&quot;image-20241227204659985&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov ax, [bx][di]
mov ax, [bx+di]
mov ax, es:[bx][si]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;p&gt;必须是一个基址寄存器和一个变址寄存器的组合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;相对基址变址寻址&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241227205126075.png&quot; alt=&quot;image-20241227205126075&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov ax, mask[bx][si]
mov ax, mask[bx+si]
mov ax, [mask+bx+si]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;汇编语言语法&lt;/h1&gt;
&lt;h2&gt;标志位&lt;/h2&gt;
&lt;h2&gt;伪指令&lt;/h2&gt;
&lt;h3&gt;1. 数据定义伪指令：&lt;code&gt;DB&lt;/code&gt;、&lt;code&gt;DW&lt;/code&gt;、&lt;code&gt;DD&lt;/code&gt;...&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; DB - 定义字节(1字节)
DATA_BYTE   DB   100         ; 定义一个字节，值为100
; DW - 定义字(2字节)
DATA_WORD   DW   100, 100H   ; 定义两个字
; 重复定义
DATA_DUP    DB   100 DUP (?) ; 定义100个未初始化的字节
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 字符串定义：&lt;code&gt;MESS1 DB &apos;AB&apos;&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;MESS1   DB   &apos;AB&apos;    ; 存储为两个字节：41H, 42H
MESS2   DW   &apos;AB&apos;    ; 存储为一个字：4241H
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 类型转换指令：&lt;code&gt;PTR&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;OPER1   DB   1, 2
OPER2   DW   1234H, 5678H

; 错误示例
MOV  AX, OPER1+1     ; 错误：类型不匹配
MOV  AL, OPER2       ; 错误：类型不匹配

; 正确用法
MOV  AX, WORD PTR OPER1+1   ; 将字节数据作为字访问
MOV  AL, BYTE PTR OPER2     ; 将字数据作为字节访问
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 标号定义：&lt;code&gt;LABEL&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; 定义一个数据区可以用两种不同的方式访问
BYTE_ARRAY    LABEL   BYTE
WORD_ARRAY    DW      50 DUP (?)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 常量定义伪指令：&lt;code&gt;EQU&lt;/code&gt;、&lt;code&gt;=&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; EQU - 定义常量，不能重定义
CONSTANT    EQU    256
ALPHA       EQU    7
BETA        EQU    ALPHA-2

; &quot;=&quot; - 可重定义的常量
EMP = 7
EMP = EMP + 1    ; 允许重新赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EQU&lt;/code&gt; (equate) 是一个比较严格的常量定义伪指令。&quot;不能重定义&quot;的意思是，一旦你使用 &lt;code&gt;EQU&lt;/code&gt; 定义了一个常量，就不能再给这个常量赋新的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 这样是正确的 - 第一次定义
ALPHA  EQU  7

; 这样是错误的 - 试图重定义 ALPHA
ALPHA  EQU  8    ; 汇编器会报错！

; 这样也是错误的
ALPHA  EQU  ALPHA + 1    ; 汇编器会报错！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相比之下，使用&lt;code&gt;=&lt;/code&gt;定义的常量是可以重新定义的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 使用 = 定义常量
COUNT = 7
; 可以重定义
COUNT = 8           ; 正确！
COUNT = COUNT + 1   ; 正确！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种区别在实际编程中很重要：如果你想定义一个绝对不应该改变的常量（比如硬件端口地址），应该用 &lt;code&gt;EQU&lt;/code&gt;；如果你需要一个可以更新的计数器或变量，应该用 &quot;&lt;code&gt;=&lt;/code&gt;&quot;&lt;/p&gt;
&lt;p&gt;这就像现代高级语言中的：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt;（常量，不能改变）相当于 &lt;code&gt;EQU&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;let/var&lt;/code&gt;（变量，可以改变）相当于 &quot;&lt;code&gt;=&lt;/code&gt;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6. 地址对齐伪指令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; ORG - 设置起始地址
ORG  100H
START:  

; EVEN - 偶地址对齐
A    DB    &apos;morning&apos;
EVEN                    ; 确保下一个数据从偶地址开始
B    DW    2 DUP (?)

; ALIGN - 按指定边界对齐
ALIGN 4                 ; 按4字节边界对齐
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 地址计数器：&lt;code&gt;$&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;; 跳过空间
ORG  $+8               ; 跳过8个字节

; 在数组中使用
ARRAY  DW  1, 2, $+4, 3, 4, $+4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 基数控制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.RADIX  16             ; 设置默认数字为16进制
MOV  BX, 0FF          ; 16进制的FF
MOV  BX, 178D         ; 10进制的178 (D后缀表示十进制)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Codeforces Round 992 (Div. 2) solutions (A-D)</title><link>https://fuwari.vercel.app/posts/acm/codeforces-round-992-div-2-solutions-a-d/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/codeforces-round-992-div-2-solutions-a-d/</guid><pubDate>Mon, 09 Dec 2024 03:23:38 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h1&gt;&lt;a href=&quot;https://codeforces.com/contest/2040&quot;&gt;Codeforces Round 992 (Div. 2) &lt;/a&gt;solutions (A-D)&lt;/h1&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2040/problem/A&quot;&gt;A - Game of Division&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;如果两个数的差值的绝对值能被 $k$ 整除，也就意味着这两个数对 $k$ 同余。&lt;/p&gt;
&lt;p&gt;那就变成了求所有数中是否存在对 $k$ 同余只有自身的数了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=100;

int n;
int a[maxn+5],k;

void solve(void){
    vector&amp;lt;int&amp;gt; cnt(maxn+5,0);
    vector&amp;lt;int&amp;gt; pos(maxn+5,0);
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    for (int i=1;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;a[i];
        a[i]%=k;
        debug(a[i]);
        cnt[a[i]]++;
        pos[a[i]]=i;
    }

	for (int i=0;i&amp;lt;=maxn;i++){
        debug(i,cnt[i]);
        if (cnt[i]==1){
            cout&amp;lt;&amp;lt;&quot;YES&quot;&amp;lt;&amp;lt;endl;
            cout&amp;lt;&amp;lt;pos[i]&amp;lt;&amp;lt;endl;
            return ;
        }
    }
    cout&amp;lt;&amp;lt;&quot;NO&quot;&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2040/problem/B&quot;&gt;B - Paint a Strip&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;观察发现，我们从1开始向上以最快的速度扩展，首先我们要选两个数做操作1，然后把里面包裹的全部用操作2转成1，然后继续选数做操作1……最快的无疑是选&lt;code&gt;1,4&lt;/code&gt;做操作1，然后把&lt;code&gt;1~4&lt;/code&gt;都转成1，然后选&lt;code&gt;10&lt;/code&gt;做操作1（现在&lt;code&gt;1~10&lt;/code&gt;有5个1了，完全可以把&lt;code&gt;1~10&lt;/code&gt;都变成1），然后再&lt;code&gt;1~10&lt;/code&gt;做操作2，然后选&lt;code&gt;22&lt;/code&gt;做操作1……&lt;/p&gt;
&lt;p&gt;也就是说，我们在初始状态后，在&lt;code&gt;1~p&lt;/code&gt;都是1的情况下，我们选&lt;code&gt;(p+1)*2&lt;/code&gt;做操作1，然后把&lt;code&gt;1~(p+1)*2&lt;/code&gt;都用操作2变1，是最优的。这个是指数增长的，预处理一下即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=1e5;

int n;

set&amp;lt;P&amp;gt; ans;
void prepare(){
    ans.insert(P{1,1});
    ans.insert(P{4,2});
    ans.insert(P{10,3});
    int pos=10,ret=3;
    while (true){
        pos=(pos+1)*2;
        ret++;
        ans.insert(P{pos,ret});
        if (pos&amp;gt;maxn)
            break;
    }
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    if (n==1)
        cout&amp;lt;&amp;lt;1&amp;lt;&amp;lt;endl;
    else if (n&amp;lt;=4)
        cout&amp;lt;&amp;lt;2&amp;lt;&amp;lt;endl;
    else{
        auto it=ans.lower_bound(P{n,0});
        cout&amp;lt;&amp;lt;it-&amp;gt;second&amp;lt;&amp;lt;endl;
    }
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
    prepare();
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2040/problem/C&quot;&gt;C - Ordered Permutations&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;首先最大化得分的构造方式，肯定是让1尽可能处于最少的区间中，2处于第二少的区间中，以此类推……&lt;/p&gt;
&lt;p&gt;那1必然只能是第一个或者最后一个，2必然是除了1以外的区间中的第一个或者最后一个，以此类推。&lt;/p&gt;
&lt;p&gt;因此我们从1到n逐个放。1放第一个或者最后一个，有2种选择；2放除了1以外的区间的第一个或者最后一个，有2种选择……只有最后的$n$，只有一种选择，因此我们能构造出来的序列个数最大是 $2^{n-1}$，这样就能输出-1的情况了。&lt;/p&gt;
&lt;p&gt;取字典序时，非常显然，假设总共个数是 $L=2^{n-1}$， 那1放第一个的序列们的字典序，必然小于1放最后一个的序列们的字典序。也就是说，1放开头和放最后两种，一定是字典序的前半部分和后半部分。那对于k，就不断判断它是在当前字典序的前半部分还是后半部分，即可确定当前数的位置，循环这样做就行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2e5;

ll n,k;
ll ans[maxn+5];

bool check_low(ll &amp;amp;k,ll i){  // k&amp;lt;=2^(n-i-1)
    ll l=log2(k);
    if (l&amp;lt;n-i-1){
        return true;
    }
    else{
        ll tmp=(1ll&amp;lt;&amp;lt;(n-i-1));
        if (k&amp;gt;tmp){
            k-=tmp;
            return false;
        }
        else
            return true;
    }
}

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    if (n&amp;lt;41){
        if ((1LL&amp;lt;&amp;lt;(n-1ll))&amp;lt;k){
            cout&amp;lt;&amp;lt;-1&amp;lt;&amp;lt;endl;
            return ;
        }
    }
    int l=1,r=n;
    ll i=1;
    while (l&amp;lt;r){
        if (check_low(k,i)){
            ans[l]=i;
            l++;
        }
        else{
            ans[r]=i;
            r--;
        }
        i++;
    }
    ans[l]=n;
    for (int i=1;i&amp;lt;=n;i++)
        cout&amp;lt;&amp;lt;ans[i]&amp;lt;&amp;lt;&apos; &apos;;
	cout&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2040/problem/D&quot;&gt;D - Non Prime Tree&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;首先猜一手不存在不能构造出来的情况（不知道这个怎么证，但是看见 2*n 时候就猜应该是有神秘结论）。&lt;/p&gt;
&lt;p&gt;这题看着是构造，其实不构造也行，理由是知道质数的个数对于大范围内的数其实是比较少的，随便取一个数较大概率不是质数，所以直接暴力枚举即可。1不是质数，所以一个合理的方法是一条链上直接1 2 3 4 地赋值。那树的其他情况怎么办？我们列一个集合，往里面塞1到2n，首先DFS遍历一下，因为DFS遍历的缘故，所以我们第一条链肯定是1 2 3 4 ...地赋值，然后切换到其他分支的时候，就继续从集合中取下一个数，判断一下和父亲节点的差值是否是质数，不是就丢掉，继续取下一个数，直到取到可以的，那它继续向下遍历的时候，下一条链大概率也是逐个递增的。对于丢掉的数，我们换一个集合保存起来，这些数可能会呈现一种相对来说部分连续的段。当前集合空了，就换成前面的保存下来的丢弃的数的集合，也就是两个集合来回切换。&lt;/p&gt;
&lt;p&gt;（这里集合我一开始用set，狠狠TLE唐完了，赛后才发现直接queue就能过）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2e5;

int n;
vector&amp;lt;int&amp;gt; G[maxn+5];
queue&amp;lt;int&amp;gt; que[2];
int op;
int ans[maxn+5];

void clear(){
    for (int i=1;i&amp;lt;=n;i++){
        G[i].clear();
        ans[i]=0;
    }
    op=0;
    while (!que[0].empty())
        que[0].pop();
    while (!que[1].empty())
        que[1].pop();
}

bool flag[maxn*2+5];
#define isprime(x) (flag[x]==false)
void prepare(){
    int lim=maxn*2;
    flag[1]=true; 
    for (int i=2;i&amp;lt;=lim;i++)
        if (isprime(i)){
            for (int j=2;i*j&amp;lt;=lim;j++)
                flag[i*j]=true;
        }
}

bool dfs(int u,int fa){
    if (fa){
        ans[u]=-1;
        while (true){
            if (que[op].empty())
                op^1;
            else if (!isprime(abs(que[op].front()-ans[fa])))
                break;
            que[op^1].push(que[op].front());
            que[op].pop();
        }
        ans[u]=que[op].front();
        que[op].pop();
    }
    else{
        ans[u]=1;
    }
    for (int v: G[u])
        if (v!=fa){
            if (dfs(v,u)==false)
                return false;
        }
    return true;
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    clear();
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    for (int i=2;i&amp;lt;=2*n;i++){
        que[0].push(i);
    }
    if (dfs(1,0)){
        for (int i=1;i&amp;lt;=n;i++)
            cout&amp;lt;&amp;lt;ans[i]&amp;lt;&amp;lt;&apos; &apos;;
        cout&amp;lt;&amp;lt;endl;
    }
    else
        cout&amp;lt;&amp;lt;-1&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
    prepare();
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>2019年全国大学生电子设计竞赛 综合测评题</title><link>https://fuwari.vercel.app/posts/embed/edc-2019-reexam/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/edc-2019-reexam/</guid><pubDate>Wed, 13 Nov 2024 02:09:36 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;很久没做模电了，学校电赛选拔要做这个，重新来复习一下模电。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;材料&lt;/h2&gt;
&lt;p&gt;LM324AD（四运放）*1&lt;/p&gt;
&lt;p&gt;SN74LS00D（四与非门）*1&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241106164241572.png&quot; alt=&quot;image-20241106164241572&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;任务&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] 频率为 $19kHz\sim 21kHz$ 连续可调的方波脉冲信号，幅度不小于 $3.2V$&lt;/li&gt;
&lt;li&gt;[x] 与方波同频率的正弦波信号，输出电压失真度不大于 $5%$，峰峰值 $(V_{pp})$ 不小于 $1V$。&lt;/li&gt;
&lt;li&gt;[x] 与方波同频率占空比 $5%\sim 15%$ 连续可调的窄脉冲信号，幅度不小于 $3.2V$&lt;/li&gt;
&lt;li&gt;[x] 与正弦波正交的余弦波信号，相位误差不大于 $5\degree$，输出电压峰峰值 $(V_{pp})$ 不小于 $1V$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各路信号输出必须引至测评板标注位置并需要接入 $1k\Omega$ 负载电阻 $R_L$.&lt;/p&gt;
&lt;p&gt;原则上不允许在测评板上增加使用 BJT、FET 和二极管。&lt;/p&gt;
&lt;h2&gt;电路&lt;/h2&gt;
&lt;h3&gt;方波&lt;/h3&gt;
&lt;p&gt;用2个与非门做出 $20kHz$ 左右 峰峰值 $5V$ 的方波：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241112203011374.png&quot; alt=&quot;image-20241112203011374&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241112203024590.png&quot; alt=&quot;image-20241112203024590&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;正弦波&lt;/h3&gt;
&lt;h4&gt;方案1&lt;/h4&gt;
&lt;p&gt;这里用两个滤波器，从方波得到正弦波。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241112223058918.png&quot; alt=&quot;image-20241112223058918&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241112223109779.png&quot; alt=&quot;image-20241112223109779&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;如上，峰峰值符合要求。&lt;/p&gt;
&lt;h4&gt;方案2&lt;/h4&gt;
&lt;p&gt;过一个RC网络，得到正弦波。（我最终改成这个方案，因为仿真出来质量更好）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241116202828005.png&quot; alt=&quot;image-20241116202828005&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241116202803589.png&quot; alt=&quot;image-20241116202803589&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;窄脉冲&lt;/h3&gt;
&lt;p&gt;用很小的电容做一个微分脉冲信号，把方波的下降沿转换成窄脉冲。窄脉冲过一个与非门，转成 $5V$ 的窄波。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241113003038853.png&quot; alt=&quot;image-20241113003038853&quot; style=&quot;zoom: 80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241113003158527.png&quot; alt=&quot;image-20241113003158527&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;余弦&lt;/h3&gt;
&lt;p&gt;正弦信号过一个移相器。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241113005146324.png&quot; alt=&quot;image-20241113005146324&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/353f6a37b3beabc478f2a3937df18ca.png&quot; alt=&quot;353f6a37b3beabc478f2a3937df18ca&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;合并电路&lt;/h3&gt;
&lt;h4&gt;方案1&lt;/h4&gt;
&lt;p&gt;这是第一版方案，可以看出来余弦失真比较严重。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241113010425561.png&quot; alt=&quot;image-20241113010425561&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241113010433305.png&quot; alt=&quot;image-20241113010433305&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;方案2&lt;/h4&gt;
&lt;p&gt;第二版方案把正弦波生成优化了一下。如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241116202927152.png&quot; alt=&quot;image-20241116202927152&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241116202435143.png&quot; alt=&quot;image-20241116202435143&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>云服务器部署overleaf，实现个人latex环境搭建</title><link>https://fuwari.vercel.app/posts/others/overleaf-record/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/others/overleaf-record/</guid><pubDate>Mon, 28 Oct 2024 12:45:11 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;很喜欢overleaf，感觉比本地vscode配置的latex环境更稳定，但是在线版有时长限制，学校也没买这个，因此决定自己部署一个挂在云服务器上的overleaf。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y docker-compose

git clone https://github.com/overleaf/toolkit.git ./overleaf-toolkit
cd ./overleaf-toolkit
bin/init
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置端口&lt;/h2&gt;
&lt;p&gt;首先是经典的在厂商防火墙中打开&lt;code&gt;1222&lt;/code&gt;端口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241028045553715.png&quot; alt=&quot;image-20241028045553715&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;config/overleaf.rc&lt;/code&gt;文件中修改配置。&lt;/p&gt;
&lt;p&gt;这里我计划将overleaf的web映射到其他端口I（默认是80），下面我尝试将端口配置在&lt;code&gt;1222&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;修改文件，把这几行改成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SERVER_PRO=false
OVERLEAF_LISTEN_IP=0.0.0.0
OVERLEAF_PORT=1222
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后命令行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo bin/up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等一会出现一堆状态后，就可以浏览器访问&lt;code&gt;&amp;lt;服务器ip&amp;gt;:1222&lt;/code&gt;来检查了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20241028051833252.png&quot; alt=&quot;image-20241028051833252&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是这里是没账号密码的，可以输入&lt;code&gt;&amp;lt;服务器ip&amp;gt;:1222/launchpad&lt;/code&gt;，手动注册一下。&lt;/p&gt;
&lt;p&gt;然后可以ctrl+c关掉之前的命令了，用下列命令，挂在后台运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo bin/start
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 TexLive&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo docker exec -it sharelatex bash
tlmgr install scheme-full

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;minted包&lt;/h2&gt;
&lt;p&gt;需要安装这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install Pygments
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;中文支持&lt;/h2&gt;
&lt;p&gt;详见这两篇博客：&lt;/p&gt;
&lt;p&gt;https://www.cnblogs.com/Undefined443/p/18155463&lt;/p&gt;
</content:encoded></item><item><title>Educational Codeforces Round 170 (Rated for Div. 2) solutions (A-E)</title><link>https://fuwari.vercel.app/posts/acm/codeforces-round-edu-170-div2-solutions/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/codeforces-round-edu-170-div2-solutions/</guid><pubDate>Wed, 16 Oct 2024 15:28:17 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://codeforces.com/contest/2025&quot;&gt;Educational Codeforces Round 170 (Rated for Div. 2)&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2025/problem/A&quot;&gt;A - Two Screens&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;签到模拟题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=100;

char str[2][maxn+5];

void solve(void){
	scanf(&quot;%s%s&quot;,str[0]+1,str[1]+1);
    int n=strlen(str[0]+1),m=strlen(str[1]+1);
    int len=1;
    while (str[0][len]==str[1][len]&amp;amp;&amp;amp;len&amp;lt;=n&amp;amp;&amp;amp;len&amp;lt;=m) len++;
    if (len&amp;gt;1)
        printf(&quot;%d\n&quot;,n+m-(len-1)+1);
    else
        printf(&quot;%d\n&quot;,n+m);
}

int main(){
	int t=1;
	scanf(&quot;%d&quot;,&amp;amp;t);
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2025/problem/B&quot;&gt;B - Binomial Coefficients, Kind Of&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;观察发现直接输出 $2^k$ 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxt=1e5;
const ll mod=(1e9)+7;

int t;

ll n[maxt+5],k[maxt+5];

ll qpow(ll a,ll b,ll mod){
    a%=mod;
    ll res=1;
    while (b&amp;gt;0) {
        if (b&amp;amp;1)
            res=res*a%mod;
        a=a*a%mod;
        b&amp;gt;&amp;gt;=1;
    }
    return res;
}

void solve(void){
	cin&amp;gt;&amp;gt;t;
    for (int i=1;i&amp;lt;=t;i++)
        cin&amp;gt;&amp;gt;n[i];
    for (int i=1;i&amp;lt;=t;i++)
        cin&amp;gt;&amp;gt;k[i];
    for (int i=1;i&amp;lt;=t;i++){
        cout&amp;lt;&amp;lt;qpow(2LL,k[i],mod)&amp;lt;&amp;lt;endl;
    }
	
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2025/problem/C&quot;&gt;C - New Game&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;排序然后双指针模拟。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2e5;

int n,k;
int a[maxn+5];


void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    sort(a+1,a+n+1);

    int l=0;
    int type=0;
    int ans=0;
    for (int i=1;i&amp;lt;=n;i++){
        if (a[i]!=a[i-1])
            type++;
        if (a[i]-a[i-1]&amp;gt;1)
            l=i-1,type=1;
        while (type&amp;gt;k){
            l++;
            if (a[l]!=a[l+1])
                type--;
        }
        ans=max(ans,i-l);
    }
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2025/problem/D&quot;&gt;D - Attribute Checks&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;看见 $m\leq 5000$ 猜到起码是一个 $O(m^2)$ 的dp。设 $dp[i][j]$ 表示智力和力量分别为 $i$ 、$j$ 时能通过的最大属性检查次数。因为对于每个相同 $i+j$ 的 $dp[i][j]$，他们到达的时间点都是一致的，无非是得到的属性点的分配问题。&lt;/p&gt;
&lt;p&gt;那直接对每个确定的总属性点 $i+j$，算他们与总属性点 $i+j+1$ 之间，过了多少次属性检查，对属性检查的要求排个序，然后模拟即可。复杂度$O(m^2 \log n)$，但是这个 $\log$ 应该是比较小的，因为是多个总长度为 $n$ 的不同数组分批排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2e6;
const int maxm=5000;

int n,m;
vector&amp;lt;int&amp;gt; req[maxm+5][2];

int dp[maxm+5][maxm+5];

void solve(void){
    cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;m;
    int now=0;
    for (int i=1,x;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;x;
        if (x==0)
            now++;
        else
            req[now][x&amp;gt;0].push_back(abs(x));
    }
    for (int i=0;i&amp;lt;=m;i++){
        sort(req[i][0].begin(),req[i][0].end());
        sort(req[i][1].begin(),req[i][1].end());
    }
    int ans=0;
    for (int len=1;len&amp;lt;=m;len++){
        int x=len,y=len-x;
        while (x&amp;gt;=0){
            dp[x][y]=max(x&amp;gt;=1?dp[x-1][y]:0,y&amp;gt;=1?dp[x][y-1]:0);
            x--;
            y=len-x;
        }
        x=len,y=len-x;
        int i=0,j=req[len][0].size()-1;
        while (x&amp;gt;=0){
            while (j&amp;gt;=0&amp;amp;&amp;amp;req[len][0][j]&amp;gt;x)
                j--;
            while (i&amp;lt;req[len][1].size()&amp;amp;&amp;amp;req[len][1][i]&amp;lt;=y)
                i++;
            dp[x][y]+=(j+1)+(i);
            debug(x,y,dp[x][y]);
            if (len==m)
                ans=max(dp[x][y],ans);
            x--;
            y=len-x;
        }
    }
	cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/2025/problem/E&quot;&gt;E - Card Game&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;首先，要让第一个人赢，除了1外，每种花色中，第一个人的数量一定是小等于第二个人的（如果大于，那多出来的牌只能被1花色击败）。&lt;/p&gt;
&lt;p&gt;设从花色$2\sim n$，每种花色 $i$ 中，数量都有
$$
res[i]=cnt_2[i]-cnt_1[i]&amp;gt;0 &amp;amp; i\in[2,n]\
$$
而这些普通的花色，2多出来的，都要和花色1中，1多于2的来互补，也就是说
$$
-res[1]=cnt_1[i]-cnt_2[i]=\sum_{i=2}^n res[i]
$$
$2\sim n$的普通花色，每种情况都是一样的。我们设任意一种花色中，在给定牌号为 $1\sim i(i\in[1,m])$时，2比1多$j$个牌，且1每张牌都有对应的2的更小牌，情况数是$dp[i][j]$。$i$ 的情况显然可以从 $i-1$ 转移过来，且只有两种情况，一种是牌分配给1，一种是牌分配给2，而且因为 $i$ 一定比 $1\sim i-1$ 大，所以不需要特别限制“1每张牌都有对应的2的更小牌”。
$$
dp[i][j]=dp[i-1][j-1]+dp[i-1][j+1]
$$
然后要用这个来推总的情况数，暴力枚举每种加法和肯定是不行。我们设 $g[i][j]$ 表示$2\sim i$ 中，总共2比1多了 $j$ 张牌的情况数，有
$$
g[i][j]=\sum_{k=0}^jg[i-1][k]\times dp[m][j-k]
$$
最终答案就是二者互补的答案，也就是枚举花色1中1比2多$k$的情况
$$
ans=\sum_{k=0}^m dp[m][k]\times g[n][k]
$$
是一个很精彩的dp套dp。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=500;
const ll mod=998244353;

int n,m;

ll dp[maxn+5][maxn+5];
ll g[maxn+5][maxn+5];

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;m;
    dp[0][0]=1;
    for (int i=1;i&amp;lt;=m;i++)
        for (int j=0;j&amp;lt;=i;j++){
            if (j)
                dp[i][j]+=dp[i-1][j-1],dp[i][j]%=mod;
            dp[i][j]+=dp[i-1][j+1],dp[i][j]%=mod;
        }
    g[1][0]=1;
    for (int i=2;i&amp;lt;=n;i++)
        for (int j=0;j&amp;lt;=m;j++)
            for (int k=0;k&amp;lt;=j;k++)
                g[i][j]+=g[i-1][k]*dp[m][j-k]%mod,g[i][j]%=mod;
    ll ans=0;
    for (int k=0;k&amp;lt;=m;k++)
        ans+=dp[m][k]*g[n][k]%mod,ans%=mod;
	cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法笔记：dsu on tree（树上启发式合并）</title><link>https://fuwari.vercel.app/posts/acm/cp-note-dsu-on-tree/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/cp-note-dsu-on-tree/</guid><pubDate>Tue, 15 Oct 2024 23:45:53 GMT</pubDate><content:encoded>&lt;p&gt;之前写过但是vp一把遇到后发现还是没理解清楚，狠补一下这个。&lt;/p&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] 模板题：https://www.luogu.com.cn/problem/U41492&lt;/li&gt;
&lt;li&gt;[x] 2022CCPC女生赛L题：https://codeforces.com/gym/104081/problem/L&lt;/li&gt;
&lt;li&gt;[ ] cf rate 1800：https://codeforces.com/contest/161/problem/D&lt;/li&gt;
&lt;li&gt;[ ] cf rate 2300：https://codeforces.com/contest/600/problem/E&lt;/li&gt;
&lt;li&gt;[ ] cf rate 2200：https://codeforces.com/contest/570/problem/D&lt;/li&gt;
&lt;li&gt;[ ] cf rate 2300：https://codeforces.com/contest/1009/problem/F&lt;/li&gt;
&lt;li&gt;[ ] cf rate 2100：https://codeforces.com/contest/208/problem/E&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考：https://zhuanlan.zhihu.com/p/658598885&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/U41492&quot;&gt;模板题：树上数颜色&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一棵根节点为 $1$ 的树，每个节点有不同的颜色。&lt;/p&gt;
&lt;p&gt;多次询问，每次询问查询以 $u$ 节点为根节点的子树中的不同颜色树。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=1e5;

int n;
vector&amp;lt;int&amp;gt; G[maxn+5];
int color[maxn+5];

int big[maxn+5];
int size[maxn+5];
void rebuild(int u,int fa){
    size[u]=1;
    for (int v: G[u])
        if (v!=fa){
            rebuild(v,u);
            size[u]+=size[v];
            if (size[big[u]]&amp;lt;size[v])
                big[u]=v;
        }
}

int cnt[maxn+5];
int color_size;
void add(int u,int x){
    cnt[color[u]]+=x;
    if (cnt[color[u]]==0)
        color_size--;
    else if (x==1&amp;amp;&amp;amp;cnt[color[u]]==1)
        color_size++;
}
void update(int u,int fa,int x){
    add(u,x);
    for (int v: G[u])
        if (v!=fa)
            update(v,u,x);
}

int ans[maxn+5];
void dsu_on_tree(int u,int fa,bool keep){
    for (int v: G[u])
        if (v!=fa&amp;amp;&amp;amp;v!=big[u])
            dsu_on_tree(v,u,false);
    if (big[u])
        dsu_on_tree(big[u],u,true);
    add(u,1);
    for (int v: G[u])
        if (v!=fa&amp;amp;&amp;amp;v!=big[u])
            update(v,u,1);
    ans[u]=color_size;
    if (keep==false){
        add(u,-1);
        for (int v: G[u])
            if (v!=fa)
                update(v,u,-1);
    }
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;color[i];
    rebuild(1,0);
	dsu_on_tree(1,0,true);
    int q,u;
    cin&amp;gt;&amp;gt;q;
    while (q--){
        cin&amp;gt;&amp;gt;u;
        cout&amp;lt;&amp;lt;ans[u]&amp;lt;&amp;lt;endl;
    }
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/gym/104081/problem/L&quot;&gt;L. 彩色的树&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;也是统计子树颜色，但是这里的答案，要求是每个子树中深度不超过 $k$ 的。&lt;/p&gt;
&lt;p&gt;可以把每个节点与它深度恰好为 $k+1$ 的重子树节点都连起来，然后统计前动态删掉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=1e5;
const int inf=0x3fffffff;

int n,k;
int color[maxn+5];

vector&amp;lt;int&amp;gt; G[maxn+5],T[maxn+5];

int sz[maxn+5];
int son[maxn+5];
void rebuild(int u,int fa){
    sz[u]=1;
    for (int v: G[u])
        if (v!=fa){
            rebuild(v,u);
            sz[u]+=sz[v];
            if (sz[son[u]]&amp;lt;sz[v])
                son[u]=v;
        }
}

int line[maxn+5],len;
void connect(int u,int fa){
    line[++len]=u;
    debug(u,len);
    if (len-k-1&amp;gt;0&amp;amp;&amp;amp;son[line[len-k-1]]==line[len-k]){
        debug(line[len-k-1],u);
        T[line[len-k-1]].push_back(u);
    }
    for (int v: G[u])
        if (v!=fa)
            connect(v,u);
    len--;
}


int cnt[maxn+5];
int color_sz;

void revise(int u,int x){
    int c=color[u];
    cnt[c]+=x;
    if (cnt[c]==0)
        color_sz--;
    else if (x==1&amp;amp;&amp;amp;cnt[c]==1)
        color_sz++;
}
void update(int u,int fa,int x,int depth,int lim=k){
    if (depth&amp;gt;lim)
        return ;
    revise(u,x);
    for (int v: G[u])
        if (v!=fa)
            update(v,u,x,depth+1,lim);
}

int ans[maxn+5];
void dsu_on_tree(int u,int fa,bool keep){
    for (int v: G[u])
        if (v!=fa&amp;amp;&amp;amp;v!=son[u])
            dsu_on_tree(v,u,false);
    if (son[u])
        dsu_on_tree(son[u],u,true);
    revise(u,1);
    for (int v: G[u])
        if (v!=fa&amp;amp;&amp;amp;v!=son[u])
            update(v,u,1,1);
    for (int v: T[u]){
        debug(u,v);
        revise(v,-1);
    }
    ans[u]=color_sz;
    if (keep==false){
        revise(u,-1);
        update(son[u],u,-1,1);
        for (int v: G[u])
            if (v!=fa&amp;amp;&amp;amp;v!=son[u])
                update(v,u,-1,1);
    }
}

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;color[i];
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    rebuild(1,0);
    connect(1,0);
        dsu_on_tree(1,0,true);
    int q,u;
    cin&amp;gt;&amp;gt;q;
    while (q--){
        cin&amp;gt;&amp;gt;u;
            cout&amp;lt;&amp;lt;ans[u]&amp;lt;&amp;lt;endl;
    }
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Codeforces板刷记录（1800~2000， 构造题）</title><link>https://fuwari.vercel.app/posts/acm/codeforces-exercise-1800-constructive-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/codeforces-exercise-1800-constructive-1/</guid><pubDate>Fri, 27 Sep 2024 15:52:51 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1981/C&quot;&gt;1981C - Turtle and an Incomplete Sequence&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1800&lt;/td&gt;
&lt;td&gt;思路很简单，想到了。但是有点模拟了，代码能力还是不够，写了80min.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1994/D&quot;&gt;1994D - Funny Game&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1900&lt;/td&gt;
&lt;td&gt;没有想到。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1956/D&quot;&gt;1956D - Nene and the Mex Operator&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;看了一点提示想到了。实现起来也花了挺久。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1951/D&quot;&gt;1951D - Buying Jewels&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;完全想歪了，不会。构造确实妙。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/contest/1770/problem/D&quot;&gt;D - Koxia and Game&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;思路很简单，想到了。实现起来有点难受，代码能力还是不够。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/contest/1763/problem/C&quot;&gt;C - Another Array 问题&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;这个构造我确实想不到。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1991/D&quot;&gt;1991D - Prime XOR Coloring&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1900&lt;/td&gt;
&lt;td&gt;想到了但没完全想到。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/contest/1946/problem/D&quot;&gt;D - Birthday Gift&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1900&lt;/td&gt;
&lt;td&gt;我是纯baka了，一开始没想到，后面写出来了也调了好久。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1895/D&quot;&gt;1895D - XOR Construction&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1900&lt;/td&gt;
&lt;td&gt;这题写的很顺，思路也很顺，30min内写完，但是用不是正解的写法。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/contest/1842/problem/D&quot;&gt; D - Tenzing and His Animal Friends&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1900&lt;/td&gt;
&lt;td&gt;想不到从最短路入手，思维还是不够。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1981/C&quot;&gt;1981C - Turtle and an Incomplete Sequence&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,ll&amp;gt; P;
const int maxn=2e5;

int n;
ll a[maxn+5];
ll b[maxn+5];

vector&amp;lt;ll&amp;gt; tmp;
int need(ll x,ll y){
    ll xx=x,yy=y;
    // debug(x,y);
    if (x&amp;lt;y)
        swap(x,y);
    vector&amp;lt;bool&amp;gt; X,Y;
    while (x)
        X.push_back(x&amp;amp;1),x&amp;gt;&amp;gt;=1;
    while (y)
        Y.push_back(y&amp;amp;1),y&amp;gt;&amp;gt;=1;
    reverse(X.begin(),X.end());
    reverse(Y.begin(),Y.end());
    int i,j;
    for (i=0,j=0;i&amp;lt;X.size()&amp;amp;&amp;amp;j&amp;lt;Y.size();i++,j++){
        if (X[i]!=Y[j])
            break;
    }
    int ret=Y.size()-j+(X.size()-j);
    tmp.clear();
    xx=min(xx,yy);
    tmp.push_back(xx);
    // debug(Y.size()-j+(X.size()-j),i,j);
    int l=Y.size()-j;
    // debug(l,xx);
    while (l){
        xx&amp;gt;&amp;gt;=1;
        debug(xx);
        tmp.push_back(xx),l--;
    }
    for (;j&amp;lt;X.size();j++){
        xx&amp;lt;&amp;lt;=1;
        xx|=X[j];
        tmp.push_back(xx);
    }
    return ret;
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    vector&amp;lt;P&amp;gt; vec;
    for (int i=1;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;a[i];
        if (a[i]!=-1){
            vec.push_back(P{i,a[i]});
            b[i]=a[i];
        }
    }
    int m=vec.size();
    if (m==0){
        bool vis=false;
        for (int i=1;i&amp;lt;=n;i++,vis^=1)
            cout&amp;lt;&amp;lt;(vis?1:2)&amp;lt;&amp;lt;&apos; &apos;;
        cout&amp;lt;&amp;lt;endl;
        return ;
    }
    for (int i=0;i+1&amp;lt;m;i++){
        debug(i);
        if (vec[i].first+1==vec[i+1].first){
            if (!(vec[i].second==(vec[i+1].second/2)||vec[i+1].second==(vec[i].second/2))){
                cout&amp;lt;&amp;lt;-1&amp;lt;&amp;lt;endl;
                return ;
            }
            continue;
        }
        int delta=need(vec[i].second,vec[i+1].second);
        // debug(delta);
        // debug(delta&amp;gt;(vec[i+1].first-vec[i].first),((vec[i+1].first-vec[i].first-delta)&amp;amp;1));
        if (delta&amp;gt;(vec[i+1].first-vec[i].first)||((vec[i+1].first-vec[i].first-delta)&amp;amp;1)){
            cout&amp;lt;&amp;lt;-1&amp;lt;&amp;lt;endl;
            return ;
        }
        int j=vec[i].first+1;
        if (tmp.front()!=vec[i].second)
            reverse(tmp.begin(),tmp.end());
        tmp.pop_back();
        int k=1;
        while (k&amp;lt;tmp.size()){
            b[j]=tmp[k];
            k++;
            j++;
        }
        bool vis=true;
        while (j&amp;lt;vec[i+1].first){
            if (vis)
                b[j]=b[j-1]&amp;lt;&amp;lt;1;
            else
                b[j]=b[j-1]&amp;gt;&amp;gt;1;
            vis^=1;
            j++;
        }
    }
    bool vis=(vec.front().second&amp;gt;=2)?true:false;
    for (int i=vec.front().first-1;i;i--){
        if (vis)
            b[i]=b[i+1]&amp;gt;&amp;gt;1;
        else
            b[i]=b[i+1]&amp;lt;&amp;lt;1;
        vis^=1;
    }
    vis=(vec.back().second&amp;gt;=2)?true:false;
    for (int i=vec.back().first+1;i&amp;lt;=n;i++){
        if (vis)
            b[i]=b[i-1]&amp;gt;&amp;gt;1;
        else
            b[i]=b[i-1]&amp;lt;&amp;lt;1;
        vis^=1;
    }
    for (int i=1;i&amp;lt;=n;i++)
        cout&amp;lt;&amp;lt;b[i]&amp;lt;&amp;lt;&apos; &apos;;
	cout&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--){
        debug(t);
        solve();
    }
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1994/D&quot;&gt;1994D - Funny Game&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;做过类似的，但是这次还是没想到。妙就妙在看见把一个不断增加的 $x$ 看作模数时，要想起鸽笼原理。所以这个是必定有解的，有点诈骗了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2000;

int n;
int a[maxn+5];

struct DSU {
    vector&amp;lt;int&amp;gt; fa;
    DSU(int n): fa(n){  // 创建n个集合，编号从0开始
        iota(fa.begin(),fa.end(),0);
    }
    int find(int x){
        return (x==fa[x])?x:(fa[x]=find(fa[x]));
    }
    void merge(int x,int y) {
        fa[find(x)]=find(y);
    }
};

void solve(void){
	cin&amp;gt;&amp;gt;n;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    DSU dsu(n+1);
    stack&amp;lt;P&amp;gt; ans;
    for (int m=n-1;m;m--){
        map&amp;lt;int,P&amp;gt; mp;  // mod, P(set,u)
        for (int u=1;u&amp;lt;=n;u++){
            int x=a[u]%m;
            if (mp.find(x)!=mp.end()){
                if (mp.find(x)-&amp;gt;second.first!=dsu.find(u)){
                    ans.push(P{u,mp.find(x)-&amp;gt;second.second});
                    dsu.merge(dsu.find(u),mp.find(x)-&amp;gt;second.second);
                    break;
                }
            }
            else
                mp[x]=P{dsu.find(u),u};
        }
    }
    cout&amp;lt;&amp;lt;&quot;YES&quot;&amp;lt;&amp;lt;endl;
    while (ans.empty()==false){
        cout&amp;lt;&amp;lt;ans.top().first&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;ans.top().second&amp;lt;&amp;lt;endl;
        ans.pop();
    }
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1956/D&quot;&gt;1956D - Nene and the Mex Operator&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;如果一个区间 $[l,r]$ 内没有0，那操作后肯定会创造出一个0。这时候如果0旁边没有$1$，继续操作肯定也会创造出 $1$。&lt;/p&gt;
&lt;p&gt;那很显然，长度为 $n$ 的数组，最后得到的答案一定满足
$$
ans\geq n^2
$$
也就是说我们有机会构造出全部都是 $n$ 的序列。&lt;/p&gt;
&lt;p&gt;如果一个数 $a_i&amp;gt; n$。它只要处于任何一个操作区间内，是一定会被降低的。那很容易想到，考虑到 $n$ 的规模很小，每个数只有两种情况（允许处于操作区间内，不允许处于操作区间内）。那就枚举每种情况，1表示不允许处于操作区间内，0表示允许处于操作区间内。&lt;/p&gt;
&lt;p&gt;假设情况是1001001000，那就对允许的三个长度分别为2、2、3的区间，计算每个区间内经过操作能达到的最大和是多少。计算可以用区间dp。&lt;/p&gt;
&lt;p&gt;设 $dp[l][r]$ 表示区间 $[l,r]$ 内能达到的最大和
$$
dp[l][r]=\max_{k\in [l,r]} {dp[l][k]+(r-k)^2}
$$
时间复杂度能过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=18;

int n;
int a[maxn+5];
int pre[maxn+5];
#define sum(l,r) (pre[r]-pre[(l)-1])

int dp[maxn+5][maxn+5];
int from[maxn+5][maxn+5];

stack&amp;lt;P&amp;gt; stk;
void dfs(int l,int r){
    debug(l,r,from[l][r]);
    int k=from[l][r];
    if (k==0)
        return ;
    if (k==r)
        k=l-1;
    stk.push(P{k+1,r});
    if (k!=l-1)
        dfs(l,k);
}

vector&amp;lt;P&amp;gt; vec;

/* 把[l,r]变成[0,r-l] */
void dfs2(int l,int r){
    if (l&amp;gt;r)
        return ;
    if (a[r]!=r-l){
        dfs2(l,r-1);
        vec.push_back(P{l,r});
        for (int i=l;i&amp;lt;=r;i++)
            a[i]=r-l;
    }
    dfs2(l,r-1);
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    for (int i=1;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;a[i];
        pre[i]=pre[i-1]+a[i];
    }
    memset(from,0,sizeof(from));
    for (int i=1;i&amp;lt;=n;i++)
        for (int j=i;j&amp;lt;=n;j++){
            dp[i][j]=sum(i,j);
            if ((j-i+1)*(j-i+1)&amp;gt;dp[i][j]){
                dp[i][j]=(j-i+1)*(j-i+1);
                from[i][j]=j;
            }
        }
    for (int len=2;len&amp;lt;=n;len++)
        for (int l=1;l+len-1&amp;lt;=n;l++){
            int r=l+len-1;
            for (int k=l;k&amp;lt;r;k++)
                if (dp[l][r]&amp;lt;dp[l][k]+(r-k)*(r-k)){
                    dp[l][r]=dp[l][k]+(r-k)*(r-k);
                    from[l][r]=k;
                }
        }
    int lim=(1&amp;lt;&amp;lt;n)-1;
    P ans={-1,0};  // value, mask
    for (int mask=0;mask&amp;lt;=lim;mask++){
        int l=0;
        int now=0;
        for (int i=1;i&amp;lt;=n;i++)
            if (mask&amp;amp;(1&amp;lt;&amp;lt;(i-1))){
                if (i-l-1&amp;gt;0)
                    now+=dp[l+1][i-1];
                l=i;
                now+=a[i];
            }
        if (n-l&amp;gt;0)
            now+=dp[l+1][n];
        if (now&amp;gt;ans.first)
            ans=P{now,mask};
    }
    cout&amp;lt;&amp;lt;ans.first&amp;lt;&amp;lt;&apos; &apos;;
    int mask=ans.second;
    int l=0;
    for (int i=1;i&amp;lt;=n;i++)
        if (mask&amp;amp;(1&amp;lt;&amp;lt;(i-1))){
            if (i-l-1&amp;gt;0)
                dfs(l+1,i-1);
            l=i;
        }
    if (n-l&amp;gt;0)
        dfs(l+1,n);
    vec.clear();
    while (!stk.empty()){
        int l=stk.top().first,r=stk.top().second;
        stk.pop();
        dfs2(l,r);
        for (int i=l;i&amp;lt;=r;i++)
            a[i]=r-l;
        vec.push_back(P{l,r});
    }
    cout&amp;lt;&amp;lt;vec.size()&amp;lt;&amp;lt;endl;
    for (P p: vec)
        cout&amp;lt;&amp;lt;p.first&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;p.second&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1951/D&quot;&gt;1951D - Buying Jewels&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;

ll n,k;

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    if (n&amp;lt;k)
        cout&amp;lt;&amp;lt;&quot;NO&quot;&amp;lt;&amp;lt;endl;
    else if (n==k)
        cout&amp;lt;&amp;lt;&quot;YES\n1\n1\n&quot;&amp;lt;&amp;lt;endl;
    else{
        ll t=1;
        if ((t+1)*((n-k+t)/t)&amp;gt;n){
            cout&amp;lt;&amp;lt;&quot;YES\n2&quot;&amp;lt;&amp;lt;endl;
            cout&amp;lt;&amp;lt;((n-k+t)/t)&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;1&amp;lt;&amp;lt;endl;
        }
        else
            cout&amp;lt;&amp;lt;&quot;NO&quot;&amp;lt;&amp;lt;endl;
    }
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1770/problem/D&quot;&gt;D - Koxia and Game&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const ll mod=998244353;
const int maxn=1e5;

int n;
int a[maxn+5],b[maxn+5];

int self_cir,cir;

struct Edge{
    int u,v;
    bool vis;
} edge[maxn+5];
#define next(from,eptr) ((eptr-&amp;gt;u==eptr-&amp;gt;v)?(eptr-&amp;gt;u):((eptr-&amp;gt;u)^(eptr-&amp;gt;v)^(from)))
vector&amp;lt;Edge*&amp;gt; G[maxn+5];

int flag[maxn+5];
bool dfs(int u,const int tag){
    flag[u]=tag;
    debug(u,tag);
    bool ret=false;
    for (Edge* e: G[u]){
        if (e-&amp;gt;vis==false){
            int v=next(u,e);
            if (flag[v]==tag)
                cir++;
            e-&amp;gt;vis=true;
            if (flag[v])
                ret=true;
            ret|=dfs(v,tag);
        }
    }
    return ret;
}

void expand(int u){
    flag[u]=u;
    for (Edge* e: G[u]){
        if (e-&amp;gt;vis==false){
            int v=next(u,e);
            e-&amp;gt;vis=true;
            expand(v);
        }
    }
}

void prepare(){
    self_cir=0,cir=0;
    for (int i=1;i&amp;lt;=n;i++)
        G[i].clear();
    memset(flag,0,sizeof(flag));
}

ll qpow(ll a,ll b,ll mod){
    a%=mod;
    ll res=1;
    while (b&amp;gt;0) {
        if (b&amp;amp;1)
            res=res*a%mod;
        a=a*a%mod;
        b&amp;gt;&amp;gt;=1;
    }
    return res;
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    prepare();
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;b[i];
    for (int i=1;i&amp;lt;=n;i++){
        if (a[i]==b[i])
            self_cir++;
        edge[i]=Edge{a[i],b[i],false};
        G[a[i]].push_back(&amp;amp;edge[i]);
        G[b[i]].push_back(&amp;amp;edge[i]);
    }
    for (int i=1;i&amp;lt;=n;i++){
        debug(G[i].size());
        if (G[i].size()==0){
            cout&amp;lt;&amp;lt;0&amp;lt;&amp;lt;endl;
            debug(&quot;1&quot;);
            return ;
        }
        else if (G[i].size()==1&amp;amp;&amp;amp;flag[i]==0){
            flag[i]=i;
            if (dfs(i,i)==0){
                cout&amp;lt;&amp;lt;0&amp;lt;&amp;lt;endl;
                debug(i);
                return ;
            }
        }
    }
    for (int i=1;i&amp;lt;=n;i++)
        if (flag[i]==false)
            expand(i),cir++;
    debug_array(flag,6);
    debug(self_cir,cir);
    cout&amp;lt;&amp;lt;(qpow(2LL,(ll)(cir-self_cir),mod)*qpow((ll)n,self_cir,mod))%mod&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1763/problem/C&quot;&gt;C - Another Array&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=2e5;

int n;
ll a[maxn+5];

void solve(void){
	cin&amp;gt;&amp;gt;n;
    ll maxx=LONG_LONG_MIN;
    int pos;
    for (int i=1;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;a[i];
        if (maxx&amp;lt;a[i])
            maxx=a[i],pos=i;
    }
    ll ans=0;
    if (n==1)
        ans=a[1];
    else if (n==2)
        ans=max(a[1]+a[2],abs(a[2]-a[1])*2LL);
    else if (n==3){
        if (a[1]==maxx||a[3]==maxx)
            ans=maxx*(ll)n;
        else{
            ans=max(max(a[3]+(a[2]-a[1])*2,3*(a[2]-a[1])),max(a[1]+(a[2]-a[3])*2,3*(a[2]-a[3]))),ans=max(a[1]+a[2]+a[3],ans);
            ans=max(ans,max(a[1]*3,a[3]*3));
        }
    }
    else
        ans=(ll)n*maxx;
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1991/D&quot;&gt;1991D - Prime XOR Coloring&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
const int maxn=2e5;

int n;

int start[]={0,1,2,2,3,3,4};

void solve(void){
	cin&amp;gt;&amp;gt;n;
    if (n&amp;lt;=6){
        cout&amp;lt;&amp;lt;start[n]&amp;lt;&amp;lt;endl;
        for (int i=1;i&amp;lt;=n;i++)
            cout&amp;lt;&amp;lt;start[i]&amp;lt;&amp;lt;&apos; &apos;;
    }
    else{
        cout&amp;lt;&amp;lt;4&amp;lt;&amp;lt;endl;
        for (int i=1;i&amp;lt;=n;i++)
            cout&amp;lt;&amp;lt;((i%4)?(i%4):4)&amp;lt;&amp;lt;&apos; &apos;;
    }
    cout&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1946/problem/D&quot;&gt;D - Birthday Gift&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;要求分最大的k，使得结果小于等于 $x$。其实就是在分段时，尽可能让结果小，最后判断能否小于 $x$。我一开始只想着分段最大还要小于 $x$，其实可以换个角度想。&lt;/p&gt;
&lt;p&gt;要让结果尽可能小，从高位到低位，肯定是尽可能先为0，做不到再1。要求是0，必须每个分段中该位的1的个数都是偶数。显然如果该位 $a_1$~$a_n$ 的1个数是奇数，这就是不可能做到的。只有偶数才可能做到。所以：&lt;/p&gt;
&lt;p&gt;对于每一位，如果$a_1\sim a_n$的1个数是偶数，那它不同划分方式是可以为0，也可以为1.&lt;/p&gt;
&lt;p&gt;对于每一位，如果$a_1\sim a_n$的1个数是奇数，那它无论怎么划分，最终都是奇数，结果是1.&lt;/p&gt;
&lt;p&gt;所以我们遍历 $x$ 的从高到低每一位，&lt;/p&gt;
&lt;p&gt;如果是1，且该位$a_1\sim a_n$为奇数个1，我们就不用管。&lt;/p&gt;
&lt;p&gt;如果是1，且该位$a_1\sim a_n$为偶数个1，我们进行最大划分，然后结束，直接输出结果。&lt;/p&gt;
&lt;p&gt;如果是0，且该位$a_1\sim a_n$为奇数个1，输出-1。&lt;/p&gt;
&lt;p&gt;如果是0，且该位$a_1\sim a_n$为偶数个1，就结合前面的位继续尝试划分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=1e5;
const int maxb=30;

int n,x;
int a[maxn+5];

bool isji[maxb+5][maxn+5];

#define getbit(x,k) (((x)&amp;gt;&amp;gt;(k))&amp;amp;1)

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;x;
    x++;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    for (int k=30;k&amp;gt;=0;k--){
        for (int i=1;i&amp;lt;=n;i++)
            if (getbit(a[i],k))
                isji[k][i]=isji[k][i-1]?0:1;
            else
                isji[k][i]=isji[k][i-1];
    }
    vector&amp;lt;int&amp;gt; incon;
    int ret=-1;
    for (int k=30;k&amp;gt;=0;k--){
        debug(k,getbit(x,k),isji[k][n]);
        if (getbit(x,k)==0&amp;amp;&amp;amp;isji[k][n])
            break;
        if (getbit(x,k)==1&amp;amp;&amp;amp;isji[k][n]==1)
            continue;
        incon.push_back(k);
        if (getbit(x,k)==0&amp;amp;&amp;amp;isji[k][n]==0&amp;amp;&amp;amp;k)
            continue;
        if (k==0&amp;amp;&amp;amp;getbit(x,k)==0&amp;amp;&amp;amp;isji[k][n]==0)
            break;
        debug_array(incon,incon.size());
        debug(k);
        vector&amp;lt;int&amp;gt; ans;
        for (int i=1;i&amp;lt;=n;i++){
            bool ok=true;
            for (int t: incon)
                if (isji[t][i]){
                    ok=false;
                    break;
                }
            debug(i,ok);
            if (ok)
                ans.push_back(i);
        }
        if (getbit(x,k)==1&amp;amp;&amp;amp;isji[k][n]==0)
            incon.pop_back();
        ret=max(ret,(int)ans.size());
    }
	cout&amp;lt;&amp;lt;ret&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1895/D&quot;&gt;1895D - XOR Construction&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;和前面一道很像。也是按位处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=4e5;
const int maxb=18;

int n;
int a[maxn+5],b[maxn+5];

int target[maxb+5];
int cnt[maxb+5];
#define getbit(x,k) (((x)&amp;gt;&amp;gt;(k))&amp;amp;1)

int ans[maxb+5];
int val;
vector&amp;lt;int&amp;gt; pos;

int flag[maxn+5];
int tag;

bool dfs(int u){
    if (u&amp;gt;=pos.size()){
        val=0;
        for (int k=maxb;k&amp;gt;=0;k--)
            if (ans[k])
                val|=(1&amp;lt;&amp;lt;k);
        tag++;
        for (int i=1;i&amp;lt;=n;i++)
            if (flag[val^b[i]]==tag)
                return false;
            else
                flag[val^b[i]]=tag;
        return true;
    }
    ans[pos[u]]=0;
    if (dfs(u+1))
        return true;
    ans[pos[u]]=1;
    if (dfs(u+1))
        return true;
    return false;
}

void solve(void){
	cin&amp;gt;&amp;gt;n;
    for (int i=1;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;a[i];
        b[i]=b[i-1]^a[i-1];
    }
    for (int i=0;i&amp;lt;n;i++)
        for (int b=maxb;b&amp;gt;=0;b--)
            if (getbit(i,b))
                target[b]++;
    for (int i=1;i&amp;lt;=n;i++)
        for (int k=maxb;k&amp;gt;=0;k--)
            if (getbit(b[i],k))
                cnt[k]++;
    pos.clear();
    for (int k=maxb;k&amp;gt;=0;k--)
        if (target[k]){
            if (cnt[k]*2!=n){
                if (cnt[k]==target[k])
                    ans[k]=0;
                else
                    ans[k]=1;
            }
            else
                pos.push_back(k);
        }
	dfs(0);
    for (int i=1;i&amp;lt;=n;i++)
        cout&amp;lt;&amp;lt;(val^b[i])&amp;lt;&amp;lt;&apos; &apos;;
    cout&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1842/problem/D&quot;&gt; D - Tenzing and His Animal Friends&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#define debug_container(container) (void)0
#endif 

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;ll,int&amp;gt; P;
const int maxn=100;

int n,m;
vector&amp;lt;P&amp;gt; G[maxn+5];

ll d[maxn+5];
void dijkstra(int s){
    for (int i=1;i&amp;lt;=n;i++)
        d[i]=LONG_LONG_MAX;
    priority_queue&amp;lt;P,vector&amp;lt;P&amp;gt;,greater&amp;lt;P&amp;gt;&amp;gt; que;
    que.push(P{0,s});
    d[s]=0;
    while (!que.empty()){
        P p=que.top();
        int u=p.second;
        ll dis=p.first;
        que.pop();
        if (d[u]&amp;lt;dis)
            continue;
        for (P e: G[u]){
            int v=e.second;
            ll w=e.first;
            if (d[v]&amp;gt;d[u]+w){
                d[v]=d[u]+w;
                que.push(P{d[v],v});
            }
        }
    }
}

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;m;
	for (int i=1,u,v,w;i&amp;lt;=m;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v&amp;gt;&amp;gt;w;
        G[u].push_back(P{w,v});
        G[v].push_back(P{w,u});
    }
    dijkstra(1);
    if (d[n]==LONG_LONG_MAX){
        cout&amp;lt;&amp;lt;&quot;inf&quot;&amp;lt;&amp;lt;endl;
        return ;
    }
    vector&amp;lt;P&amp;gt; dis(n);
    for (int i=1;i&amp;lt;=n;i++)
        dis[i-1]=P{d[i],i};
    sort(dis.begin(),dis.end());
    vector&amp;lt;P&amp;gt; ans;
    ll tot=0;
    for (int i=0;i&amp;lt;dis.size();i++){
        P p=dis[i];
        int u=p.second;
        if (ans.empty()==false){
            ans.back().first=p.first-dis[i-1].first;
            tot+=ans.back().first;
        }
        if (u==n)
            break;
        ans.push_back(P{0,u});
    }
    cout&amp;lt;&amp;lt;tot&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;ans.size()&amp;lt;&amp;lt;endl;
    string str(n,&apos;0&apos;);
    for (P p: ans){
        str[p.second-1]=&apos;1&apos;;
        cout&amp;lt;&amp;lt;str&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;p.first&amp;lt;&amp;lt;endl;
    }

}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
//		cout&amp;lt;&amp;lt;(solve()?&quot;YES&quot;:&quot;NO&quot;)&amp;lt;&amp;lt;endl;
	return 0;
}


&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>物联网项目记录（2） 树莓派语音与视觉基础</title><link>https://fuwari.vercel.app/posts/embed/iot-aicoach-2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/iot-aicoach-2/</guid><pubDate>Wed, 25 Sep 2024 15:05:18 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;树莓派4B*1，4g内存至少（2G就不能推广到边缘计算了，8G其实最好，不过给学校省经费用的4G），32GB存储卡。&lt;/li&gt;
&lt;li&gt;语音部分需要树莓派免驱扬声器，那种USB直连的。&lt;/li&gt;
&lt;li&gt;视觉部分需要USB摄像头，最好也是免驱的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;语音部分&lt;/h2&gt;
&lt;h3&gt;扬声器测试&lt;/h3&gt;
&lt;p&gt;在树莓派中新建项目文件夹，因为树莓派主要是下位机功能，所以我这里叫&lt;code&gt;client_rsp&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir client_rsp
cd client_rsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里推荐从终端换成vscode的remote ssh连接，写代码比较方便。&lt;/p&gt;
&lt;p&gt;配置环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install python3-pyaudio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建虚拟环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m venv --system-site-packages venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单将一个&lt;code&gt;audio.wav&lt;/code&gt;传到这个目录下，然后运行下面代码，可以测试扬声器是否正常工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pyaudio
import wave

def play_audio(filename):
    with wave.open(filename, &apos;rb&apos;) as wf:
        p = pyaudio.PyAudio()
        stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                        channels=wf.getnchannels(),
                        rate=wf.getframerate(),
                        output=True)

        chunk_size = 1024

        data = wf.readframes(chunk_size)
        while data:
            stream.write(data)
            data = wf.readframes(chunk_size)

        stream.stop_stream()
        stream.close()
        p.terminate()

play_audio(&apos;audio.wav&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有声音就是正常。&lt;/p&gt;
&lt;h3&gt;树莓派音量调节&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查询音量
amixer get Master

# 调节音量
amixer set Master 50%
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义语音输出&lt;/h3&gt;
&lt;p&gt;这里希望实现一个输入任意文字，能输出语音的接口，也就是语音合成。这个语音合成可以部署在树莓派或者服务器上，还是比较好部署的，但是我这里直接用了阿里云dashscope的api，一是比较方便，二是合成效果更好（模型好，更自然）。&lt;/p&gt;
&lt;p&gt;获取api key在这里：&lt;a href=&quot;https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key?spm=a2c4g.11186623.0.0.12a52e5cexnObO#NJCzq&quot;&gt;API-KEY的获取&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;配置阿里云DashScope的SDK：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install dashscope
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时配置一下环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install paho-mqtt
pip install pyyaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和前面一样，我们在项目文件夹下新建一个&lt;code&gt;config.yaml&lt;/code&gt;，用来填写配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dashscope:
  api-key: &quot;&amp;lt;api-key&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面代码实现了一个自定义语音输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
import dashscope
from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse
from dashscope.audio.tts import ResultCallback, SpeechSynthesizer, SpeechSynthesisResult
import pyaudio

import os
import sys
import yaml

with open(&apos;config.yaml&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)
dashscope.api_key = config[&apos;dashscope&apos;][&apos;api-key&apos;]

sport_active = False

class Callback(ResultCallback):
    _player = None
    _stream = None

    def on_open(self):
        print(&apos;Speech synthesizer is opened.&apos;)
        current_timestamp = int(time.time())
        
        self._player = pyaudio.PyAudio()
        self._stream = self._player.open(
            format=pyaudio.paInt16,
            channels=1, 
            rate=48000,
            output=True)

    def on_complete(self):
        print(&apos;Speech synthesizer is completed.&apos;)
        
    def on_error(self, response: SpeechSynthesisResponse):
        print(&apos;Speech synthesizer failed, response is %s&apos; % (str(response)))

    def on_close(self):
        print(&apos;Speech synthesizer is closed.&apos;)
        self._stream.stop_stream()
        self._stream.close()
        self._player.terminate()

    def on_event(self, result: SpeechSynthesisResult):
        if result.get_audio_frame() is not None:
            print(&apos;audio result length:&apos;, sys.getsizeof(result.get_audio_frame()))
            self._stream.write(result.get_audio_frame())

        if result.get_timestamp() is not None:
            print(&apos;timestamp result:&apos;, str(result.get_timestamp()))

callback = Callback()


while True:
    message = input(&quot;请输入要合成的文本：&quot;)
    if message:
        SpeechSynthesizer.call(model=&apos;sambert-zhiya-v1&apos;, text=message, sample_rate=48000, format=&apos;pcm&apos;, callback=callback)
    else:
        time.sleep(1) 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MQTT远程语音控制&lt;/h3&gt;
&lt;p&gt;自定义输入文字还不够，我们的项目是服务器得到运动评价后，将评语传递给树莓派，让它说话。因此要实现一个mqtt的远程语音控制。&lt;/p&gt;
&lt;p&gt;配置文件&lt;code&gt;config.yaml&lt;/code&gt;中加上MQTT的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dashscope:
  api-key: &quot;&amp;lt;api-key&amp;gt;&quot;
  model: &quot;&amp;lt;model&amp;gt;&quot;

server:
  address: &quot;&amp;lt;ip&amp;gt;&quot;         # MQTT服务器地址
  port: &amp;lt;port&amp;gt;                        # MQTT服务器端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是运行的文件&lt;code&gt;voice.py&lt;/code&gt;，实现了通过mqtt远程发送后输出声音：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
import os
import sys
import yaml

with open(&apos;config.yaml&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)

# -----------------------------------------------------------------------------

&apos;&apos;&apos;
    语音合成
&apos;&apos;&apos;

import dashscope
from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse
from dashscope.audio.tts import ResultCallback, SpeechSynthesizer, SpeechSynthesisResult
import pyaudio

dashscope.api_key = config[&apos;dashscope&apos;][&apos;api-key&apos;]

class Callback(ResultCallback):
    _player = None
    _stream = None

    def on_open(self):
        print(&apos;Speech synthesizer is opened.&apos;)
        current_timestamp = int(time.time())
        
        self._player = pyaudio.PyAudio()
        self._stream = self._player.open(
            format=pyaudio.paInt16,
            channels=1, 
            rate=48000,
            output=True)

    def on_complete(self):
        print(&apos;Speech synthesizer is completed.&apos;)
        
    def on_error(self, response: SpeechSynthesisResponse):
        print(&apos;Speech synthesizer failed, response is %s&apos; % (str(response)))

    def on_close(self):
        print(&apos;Speech synthesizer is closed.&apos;)
        self._stream.stop_stream()
        self._stream.close()
        self._player.terminate()

    def on_event(self, result: SpeechSynthesisResult):
        if result.get_audio_frame() is not None:
            print(&apos;audio result length:&apos;, sys.getsizeof(result.get_audio_frame()))
            self._stream.write(result.get_audio_frame())

        if result.get_timestamp() is not None:
            print(&apos;timestamp result:&apos;, str(result.get_timestamp()))

callback = Callback()

# -----------------------------------------------------------------------------

&apos;&apos;&apos;
    MQTT部分
&apos;&apos;&apos;

import paho.mqtt.client as mqtt

mqtt_server = config[&apos;server&apos;][&apos;address&apos;]
mqtt_port = config[&apos;server&apos;][&apos;port&apos;]
mqtt_user = &quot;rsp_voicer&quot;

mqtt_topic = &quot;rsp/voice&quot;

def on_connect(client, userdata, flags, rc):
    print(&quot;Connected with result code &quot;+str(rc))
    client.subscribe(mqtt_topic)
    client.subscribe(&quot;sport_cmd&quot;)

def on_message(client, userdata, msg):
#    global sport_active
    message = msg.payload.decode(&apos;utf-8&apos;)
    print(f&quot;Message arrived on topic: {msg.topic}. Message: {message}&quot;)
    
    if msg.topic == mqtt_topic:
        SpeechSynthesizer.call(model=config[&apos;dashscope&apos;][&apos;model&apos;], text=message, sample_rate=48000, format=&apos;pcm&apos;, callback=callback)

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

client.username_pw_set(mqtt_user)
client.connect(mqtt_server, mqtt_port, 60)

client.loop_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本上实现了这部分功能。&lt;/p&gt;
&lt;h2&gt;视觉&lt;/h2&gt;
&lt;p&gt;因为初始版本是openmv，并且为了适应低带宽像素调的很低，因此树莓派先实现了一个比较低级的版本。也就是像素进行很大程度的压缩，并且控制了采样率。&lt;/p&gt;
&lt;p&gt;配置opencv：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install python3-opencv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;略了。&lt;/p&gt;
&lt;h2&gt;树莓派脚本开机自启动&lt;/h2&gt;
&lt;p&gt;在随便哪个路径下创建一个要开机执行的命令，假设是&lt;code&gt;/&amp;lt;路径&amp;gt;/start.sh&lt;/code&gt;。写入命令后，要记得给执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x /&amp;lt;路径&amp;gt;/start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编辑这个文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/rc.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;exit 0&lt;/code&gt;前，输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;su pi -c &quot;exec /&amp;lt;路径&amp;gt;/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里意思是在pi用户下执行命令，主要是为了避免权限问题导致失败。&lt;/p&gt;
</content:encoded></item><item><title>物联网项目记录（1） 服务器配置（MQTT）</title><link>https://fuwari.vercel.app/posts/embed/iot-aicoach-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/iot-aicoach-1/</guid><pubDate>Wed, 25 Sep 2024 15:02:06 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最近在移植之前物联网的竞赛项目的时候，重新整理了一下并记录。以便后面再次移植或者传给后人（）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;需要：云服务器*1，镜像Ubuntu系统。&lt;/p&gt;
&lt;p&gt;配置上，项目最初部署在华为云上，内存8G，带宽10M/s，硬盘120G（硬盘一般用不满，主要是内存和带宽）。&lt;/p&gt;
&lt;p&gt;图传想要达到较好的质量，实测带宽10M/s是至少的，这样传递QQVGA的图像才能不明显卡顿。但是实际上图像处理可以放在边缘处理，就可以降低带宽需求。内存是考虑到运行深度学习模型需要的，但是也可以通过优化算法来降低需求。因此后期移植，我考虑的一个新的配置是内存4G，带宽6M/s，硬盘70G。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922022018669.png&quot; alt=&quot;image-20240922022018669&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h2&gt;开发环境搭建&lt;/h2&gt;
&lt;p&gt;远程连接服务器的推荐用Finalshell，集成了FTP和shell，非常好用。具体过程略。可以参考这篇文章，&lt;a href=&quot;http://t.csdnimg.cn/9V3SD&quot;&gt;这个&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这一步主要搭建EMQX，所以没什么代码量。后面面对前后端处理的HTTP服务器搭建时，就可以考虑用vscode的远程资源管理器来写代码。&lt;/p&gt;
&lt;h2&gt;EMQX配置&lt;/h2&gt;
&lt;p&gt;命令行按行逐个运行下列指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s https://assets.emqx.com/scripts/install-emqx-deb.sh | sudo bash
sudo apt-get install emqx
sudo systemctl start emqx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的端口已经全部开放，这时候浏览器输入&lt;code&gt;&amp;lt;服务器ip&amp;gt;：18083&lt;/code&gt;就可以进入EMQX的后台管理系统。但是大部分云服务器是默认不打开这些端口的，需要在安全组配置。&lt;/p&gt;
&lt;p&gt;EMQX需要哪些端口？服务器输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo lsof -i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到所有是EMQX使用的端口：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922023609152.png&quot; alt=&quot;image-20240922023609152&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;这些就是我们需要在云服务器厂商的后台开放的端口。进入云服务器厂商的后台，以腾讯云为例，找到“防火墙”（大概这个意思，华为云我记得是叫安全组）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922023203850.png&quot; alt=&quot;image-20240922023203850&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击添加规则，添加上述我们查询得知需要的端口。（为了图方便把所有端口都打开也行，但是不推荐）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922023946096.png&quot; alt=&quot;image-20240922023946096&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在浏览器输入&lt;code&gt;&amp;lt;服务器ip&amp;gt;：18083&lt;/code&gt;，就可以看见后台了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果浏览器进不了这个网页后台，要么是服务器端口没有放开（上一步有问题），要么是浏览器自动解析成了&lt;code&gt;https&lt;/code&gt;，而EMQX默认是&lt;code&gt;http&lt;/code&gt;，这时候在浏览器输入&lt;code&gt;&amp;lt;服务器ip&amp;gt;：18083&lt;/code&gt;，然后手动改成&lt;code&gt;http&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922024128957.png&quot; alt=&quot;image-20240922024128957&quot; /&gt;&lt;/p&gt;
&lt;p&gt;初始时，用户名是&lt;code&gt;admin&lt;/code&gt;，密码是&lt;code&gt;public&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;简单测试&lt;/h2&gt;
&lt;p&gt;在手头没有嵌入式设备的情况下，简单用python测试一下是否可行。&lt;/p&gt;
&lt;p&gt;在本地电脑上新建一个项目文件夹（叫什么可以，我叫&lt;code&gt;mqtt_tester&lt;/code&gt;），新建三个文件，&lt;code&gt;config.yaml&lt;/code&gt;，&lt;code&gt;sender.py&lt;/code&gt;，&lt;code&gt;listener.py&lt;/code&gt;，分别用来存储一些服务器和客户端配置文本、运行时负责模拟发送、运行时模拟接收。&lt;/p&gt;
&lt;p&gt;预先需要pip一些包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install paho-mqtt
pip install pyyaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是&lt;code&gt;config.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  address: &quot;&amp;lt;服务器ip&amp;gt;&quot;              # MQTT服务器地址
  port: 1883                        # MQTT服务器端口
  topics:                           # 可订阅主题
    - &quot;mqtt/test&quot;                       # 测试主题

client:
  name: &quot;mqtt_tester&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
    sender.py
    模拟客户端，测试MQTT服务器收发消息
&quot;&quot;&quot;

import os
import paho.mqtt.client as mqtt
import yaml

# 获取当前脚本文件的绝对路径，然后获取同目录下的config.yaml文件的路径
current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(current_dir, &apos;config.yaml&apos;)
with open(config_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)
broker_address = config[&apos;server&apos;][&apos;address&apos;] 
broker_port = config[&apos;server&apos;][&apos;port&apos;]

def on_connect(client, userdata, flags, rc):
    print(f&quot;Connected with result code {rc}&quot;)

client = mqtt.Client()  
client.on_connect = on_connect 
client.username_pw_set(config[&apos;client&apos;][&apos;name&apos;])
client.connect(broker_address, broker_port, 60)

client.loop_start()

while True:
    topic = input(&quot;请输入主题: &quot;)
    message = input(&quot;请输入消息内容: &quot;)#.encode(&apos;gbk&apos;) #后面可以选编码格式，简单测试不需要
    client.publish(topic, message)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
	listener.py
	测试MQTT监听程序
&quot;&quot;&quot;

import os
import paho.mqtt.client as mqtt
import yaml

current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(current_dir, &apos;config.yaml&apos;)

with open(&apos;config.yaml&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)
MQTT_SERVER = config[&apos;server&apos;][&apos;address&apos;] 
MQTT_PORT = config[&apos;server&apos;][&apos;port&apos;]
MQTT_TOPIC = config[&apos;server&apos;][&apos;topics&apos;]

def on_message(client, userdata, msg):
    try:
        message = msg.payload.decode(&apos;utf-8&apos;)  # 尝试用UTF-8解码
    except UnicodeDecodeError:
        message = msg.payload.decode(&apos;gbk&apos;)  # 如果失败，改用GBK解码
    topic = msg.topic
    print(f&quot;Received message &apos;{message}&apos; on topic &apos;{topic}&apos;&quot;)

def on_connect(client, userdata, flags, rc):
    print(&quot;Connected with result code &quot;+str(rc))
    for topic in MQTT_TOPIC:
        client.subscribe(topic)
    
if __name__ == &apos;__main__&apos;:
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message

    client.username_pw_set(config[&apos;client&apos;][&apos;name&apos;])
    client.connect(MQTT_SERVER, MQTT_PORT, 60)

    client.loop_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时运行上述两个&lt;code&gt;sender.py&lt;/code&gt;和&lt;code&gt;listener.py&lt;/code&gt;，在前者的控制台中输入主题和消息内容，切换到后者的控制台，就可以看见已经收到了消息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922030511802.png&quot; alt=&quot;image-20240922030511802&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922030521758.png&quot; alt=&quot;image-20240922030521758&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不要关闭控制台，打开前面的EMQX后台网页，进入客户端，可以看见两个连接的客户端。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240922030856835.png&quot; alt=&quot;image-20240922030856835&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>厦大智慧教务：获取签到码、修改定位</title><link>https://fuwari.vercel.app/posts/others/signin-record/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/others/signin-record/</guid><pubDate>Sat, 14 Sep 2024 14:38:49 GMT</pubDate><content:encoded>&lt;p&gt;流程大致分为两步：① 远程获取签到码 ② 修改定位签到。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注1：原理基于可神的签到脚本，目前还可以通过脚本获取签到码，但直接用脚本签到疑似会被hack。不确定通过脚本获取签到码之后是否也会被hack，因此记录下最朴素的方法，环境完全模拟真实签到。&lt;/p&gt;
&lt;p&gt;注2：部分课程已经采用全新签到系统，全新签到系统方法暂待摸索。&lt;/p&gt;
&lt;p&gt;注3：仅供学习，不推荐通过此方法逃避考勤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&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;ul&gt;
&lt;li&gt;微信电脑版&lt;/li&gt;
&lt;li&gt;电脑端抓包软件（例，Proxyman）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;版本二（手机版。不推荐，配置麻烦，且不同厂商的手机可能问题不一样）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;微信手机版&lt;/li&gt;
&lt;li&gt;手机版抓包软件（不需要root的有PCAPdroid等，需要root的有很多，先不举例）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;② 修改定位：&lt;/p&gt;
&lt;p&gt;版本一（苹果设备，ipad，iphone都可以）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电脑版爱思助手，与苹果设备能连通。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;版本二（如果你没有苹果设备）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电脑版，安装安卓虚拟机（什么手游模拟器也可以）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;操作流程&lt;/h2&gt;
&lt;p&gt;下列操作仅以上述的版本一为例，版本二原理都一样，可以自行探索。&lt;/p&gt;
&lt;h3&gt;获取签到码&lt;/h3&gt;
&lt;p&gt;① 电脑端微信登录。搜索&lt;code&gt;厦大智慧教务&lt;/code&gt;，进入智慧教务小程序。点击课程签到，进入下图界面：&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://imgse.com/i/pAuEP8U&quot;&amp;gt;&amp;lt;img src=&quot;https://s21.ax1x.com/2024/09/14/pAuEP8U.jpg&quot; alt=&quot;pAuEP8U.jpg&quot; border=&quot;0&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;② 进入上述界面后，打开Proxyman，启动监听。&lt;/p&gt;
&lt;p&gt;③ 进入要签到的课程，启动签到，进入这个页面：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240914142204258.png&quot; alt=&quot;image-20240914142204258&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;④ 回到Proxyman，左侧找到这条&lt;code&gt;/getXsQdInfo&lt;/code&gt;的包。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240914142225151.png&quot; alt=&quot;image-20240914142225151&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;右侧点击&lt;code&gt;Body&lt;/code&gt;，找到&lt;code&gt;klHm&lt;/code&gt;字段，即为签到码。&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://imgse.com/i/pAuEFv4&quot;&amp;gt;&amp;lt;img src=&quot;https://s21.ax1x.com/2024/09/14/pAuEFv4.png&quot; alt=&quot;pAuEFv4.png&quot; border=&quot;0&quot; style=&quot;zoom:33%;&quot;  /&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;如上图，签到码为8303。&lt;/p&gt;
&lt;h3&gt;修改定位&lt;/h3&gt;
&lt;p&gt;打开爱思助手，链接。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240914142347743.png&quot; alt=&quot;image-20240914142347743&quot; style=&quot;zoom: 25%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;工具箱搜索 虚拟定位：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240914142414877.png&quot; alt=&quot;image-20240914142414877&quot; style=&quot;zoom:25%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;设置定位后修改即可。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240914142436740.png&quot; alt=&quot;image-20240914142436740&quot; style=&quot;zoom: 33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;现在ipad已经是学武楼定位了，ipad上登录微信，打开智慧教务，签到即可。&lt;/p&gt;
</content:encoded></item><item><title>算法笔记：线性基</title><link>https://fuwari.vercel.app/posts/acm/cp-note-xor-base/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/cp-note-xor-base/</guid><pubDate>Wed, 11 Sep 2024 13:18:39 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;网络赛被线性基典题暴打了，狠狠补一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个线性基满足，对于它表示的所有数集合 $S$，集合中任意一个数异或所得到的结果都可以表示为线性基中的元素互相异或的和。或者说，线性基可以找到一组数的子集，使得这些子集的异或和能够表示出所有可能的异或结果。&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;快速查询一堆数可以异或出来的第 $k$ 大值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具有一些性质：&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;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class LinearBasis{
private:
    int BITMAX;
    bool zero;  // 是否存在（能合成或自带的0）
 
    vector&amp;lt;T&amp;gt; b;
    bool outdate;
    
public:
    vector&amp;lt;T&amp;gt; p;
    void rebuild(){
        if (outdate==false)
            return ;
        int cnt=0;
        b.clear();
        for (int i=0;i&amp;lt;BITMAX;i++){
            for (int j=i-1;j&amp;gt;=0;j--)
                if (p[i]&amp;amp;(1LL&amp;lt;&amp;lt;j))
                    p[i]^=p[j];
            if (p[i])
                b.push_back(p[i]);
        }
        outdate=false;
    }
 
    LinearBasis(int BITMAX=sizeof(T)*8):
        BITMAX(BITMAX),zero(false),outdate(false){
        p.resize(BITMAX,0);
    }
    void insert(T x){
        for (int i=BITMAX-1;i&amp;gt;=0;i--){
            if (x&amp;amp;(1LL&amp;lt;&amp;lt;i)){
                if (!p[i]){
                    p[i]=x,outdate=true;
                    return ;
                }
                x^=p[i];
            }
        }
        if (x==0)
            zero=true;
    }
    bool check(T x){
        for (int i=BITMAX-1;i&amp;gt;=0;i--)
            if (x&amp;amp;(1LL&amp;lt;&amp;lt;i)){
                if (!p[i])
                    return false;
                x^=p[i];
            }
        return true;
    }
    T query_max(){
        T ans=0;
        for (int i=BITMAX-1;i&amp;gt;=0;i--)
            if ((ans^p[i])&amp;gt;ans)
                ans^=p[i];
        return ans;
    }
    T query_min(T k=1){
        k-=zero;
        if (k==0)
            return 0;
        rebuild();
        if (k&amp;gt;=(1LL&amp;lt;&amp;lt;b.size()))
            return -1;
        ll ans=0;
        for (int i=0;i&amp;lt;b.size();i++)
            if (k&amp;amp;(1LL&amp;lt;&amp;lt;i))
                ans^=b[i];
        return ans;
    }
    int size(void){
        rebuild();
        return b.size();
    }
};
 
void solve(void){
    LinearBasis&amp;lt;int&amp;gt; lbase;	// 不知道为什么如果写成全局变量，多组数据会有点问题
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3812&quot;&gt;P3812 【模板】线性基&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;模板题，查询异或能出现的最大值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.acwing.com/problem/content/212/&quot;&gt;acwing 异或运算&lt;/a&gt; / &lt;a href=&quot;https://acm.hdu.edu.cn/showproblem.php?pid=3949&quot;&gt;hdu 3939&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;查询第k小值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3857&quot;&gt;[TJOI2008] 彩灯&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;查询异或结果的总数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/gym/105336/problem/J&quot;&gt;J - 找最小&lt;/a&gt; 2024CCPC网络赛&lt;/td&gt;
&lt;td&gt;典题。查询异或结果最大值的最小值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[P3265 &lt;a href=&quot;https://www.luogu.com.cn/problem/P3265&quot;&gt;JLOI2015]装备购买&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Todo：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.luogu.com.cn/training/11251#information&quot;&gt;线性基题单&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;参考：https://www.luogu.com.cn/problem/solution/P3812&lt;/p&gt;
</content:encoded></item><item><title>物联网：ESP01S配置MQTT的简单记录</title><link>https://fuwari.vercel.app/posts/embed/edc-esp8266-mqtt/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/edc-esp8266-mqtt/</guid><pubDate>Tue, 10 Sep 2024 15:52:24 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;整理一下之前做物联网的记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一. 固件烧录&lt;/h2&gt;
&lt;p&gt;因为没有烧录座，就直接拿USB转ttl模块来做了。&lt;/p&gt;
&lt;p&gt;连线是正常的如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ESP32-01S			USB-to-TTL
VCC			-&amp;gt;		3.3V
GND			-&amp;gt;		GND
RX			-&amp;gt;		TX
TX			-&amp;gt;		RX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，因为要进烧录模式，因此还需要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GPIO0(IO0)	-&amp;gt;		GND
EN			-&amp;gt;		3.3V
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 命令&lt;/h2&gt;
&lt;h3&gt;配置WIFI网络&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;AT+CWMODE=1
AT+CWJAP=&quot;final&quot;,&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;连接服务器&lt;/h3&gt;
&lt;p&gt;连接服务器（这句应该不需要）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT+CIPSTART=&quot;TCP&quot;,&quot;113.45.173.169&quot;,1883
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;连接MQTT服务器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;AT+MQTTUSERCFG=0,1,&quot;esp01s_id&quot;,&quot;esp01s&quot;,&quot;esp01s&quot;,0,0,&quot;/mqtt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;AT+MQTTCONN=0,&quot;&amp;lt;服务器ip&amp;gt;&quot;,&amp;lt;服务器端口&amp;gt;,1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发送命令&lt;/h2&gt;
&lt;p&gt;运动开始。（2的意思是保证服务器只收到一次，0是不保留消息，只发送给当前正在监听的服务端。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT+MQTTPUB=0,&quot;sport_cmd&quot;,&quot;start&quot;,2,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运动结束&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT+MQTTPUB=0,&quot;sport_cmd&quot;,&quot;end&quot;,2,0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;发送具体信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;AT+MQTTPUB=0,&quot;信息类型&quot;,&quot;信息数据（字符串）&quot;,2,0
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>简单实现vps搭建个人梯子的记录</title><link>https://fuwari.vercel.app/posts/others/ladder-record/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/others/ladder-record/</guid><pubDate>Tue, 03 Sep 2024 23:43:47 GMT</pubDate><content:encoded>&lt;p&gt;记录一下。&lt;/p&gt;
&lt;p&gt;① 首先买一台香港或者国外的云服务器。例如随便找一家主机厂商（不推荐腾讯云阿里云感觉有点危险，除此之外感觉都可以），我使用的是&lt;a href=&quot;https://my.henghost.com/clientarea.php&quot;&gt;这个&lt;/a&gt;，选的是Ubuntu系统，配置无所谓，个人用没关系。&lt;/p&gt;
&lt;p&gt;② 登录服务器：&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;pip&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-get update  
apt-get install python3-pip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装&lt;code&gt;shadowsocks&lt;/code&gt;服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install shadowsocks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssserver -p &amp;lt;这里是端口号&amp;gt; -k &amp;lt;这里是密码&amp;gt; -m rc4-md5 -d start

# 后面的`rc4-md5`是加密方式，其实无所谓
# 例如：
# ssserver -p 443 -k Ladder -m rc4-md5 -d start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要关闭服务器，就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssserver -d stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 需要翻墙的设备上进行客户端配置。&lt;/p&gt;
&lt;p&gt;参考：https://shadowsocksool.com/&lt;/p&gt;
&lt;p&gt;我使用的是&lt;a href=&quot;https://shadowsocksool.com/nekoray/&quot;&gt;NekoRay&lt;/a&gt;，不过其他的也可以。&lt;/p&gt;
&lt;p&gt;服务器-手动输入配置，名称随便写，地址写前面买的主机ip，端口写前面启动shadowsocks服务器时的端口号，加密方式前面启动时用了&lt;code&gt;rc4-md5&lt;/code&gt;就选这个，密码也是填前面写的。确定后点击系统代理，就完成了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wegret-pic.oss-cn-beijing.aliyuncs.com/image-20240903234140620.png&quot; alt=&quot;image-20240903234140620&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>算法笔记：异或哈希（xor-hashing）</title><link>https://fuwari.vercel.app/posts/acm/cp-note-xor-hash/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/cp-note-xor-hash/</guid><pubDate>Wed, 28 Aug 2024 22:38:25 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;异或哈希可以用来快速查询序列中的数是否出现了$k$次。&lt;/p&gt;
&lt;h2&gt;异或哈希入门：区间集合判同&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;问题：如何判断一个序列是否是另一个序列排列组合，也就是排序后是否相同，或者说元素的集合是否相同（不关心顺序）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由异或的性质（交换律），可以知道，一个序列如果排序后和另一个序列相等，这两个序列的异或结果相同，相当于一个哈希了。但实际上还有一个问题：排序后相同的序列，异或结果一定相同；但是异或结果一定相同的序列，不一定排序后相同。&lt;/p&gt;
&lt;p&gt;这个冲突的概率如何呢？假设序列长度为$n$，每种元素都由$1$到$n$的一个数编码，异或的结果也是$n$。序列长度为$n$，排序后不同的类型序列数的规模最大项是$n^2$的，映射到的哈希空间的最大项是$n$的，肯定冲突。&lt;/p&gt;
&lt;p&gt;如果想要用类似的方式的方式来快速判断，需要想办法扩大哈希空间。因此选用$1$到$2^{64}$的数编码，这样哈希空间的最大项就到了$2^{64}$，可以很大程度上减少碰撞概率。例如，$n=1e6$的时候，碰撞概率是
$$
p\approx\frac{(10^6)^2}{2^{64}}\approx 5.42\times 10^{-8}
$$
碰撞的概率还是很小的，因此这种哈希方式是可行的。出于保险还可以选择双哈希。&lt;/p&gt;
&lt;p&gt;代码（这题没有找到能交的oj测试过，本地测了一下应该对）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef unsigned long long ull;

mt19937_64 rnd(time(0));
unordered_map&amp;lt;int, ull&amp;gt; XORHash_map[2];

struct XORHashing {
    int len;
    vector&amp;lt;int&amp;gt; s;
    vector&amp;lt;ull&amp;gt; hash[2]; // 哈希值

    void init(const vector&amp;lt;int&amp;gt;&amp;amp; str){
        s=str;
        len=s.size();
        ull xort;
        for (int p=0;p&amp;lt;2;p++) {
            hash[p].resize(len+1);
            hash[p][0]=0;
            for (int i=1;i&amp;lt;=len;i++) {
                if (XORHash_map[p].find(s[i-1])==XORHash_map[p].end())
                    XORHash_map[p].insert(make_pair(s[i-1],rnd()));
                xort=XORHash_map[p][s[i-1]];
                hash[p][i]=hash[p][i-1]^xort; 
            }
        }
    }
    // 获取子字符串[s, e]的异或哈希值，序号从0开始
    pair&amp;lt;ull, ull&amp;gt; get_hash(int s, int t) {
        ull hash1=(hash[0][t+1]^hash[0][s]);
        ull hash2=(hash[1][t+1]^hash[1][s]);
        return {hash1, hash2};
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;mt19937_64&lt;/code&gt;是基于 Mersenne Twister 算法的伪随机数生成器，专门生成 64 位宽的随机数，具有较长周期（$2^{19937}-1$）。&lt;/p&gt;
&lt;h2&gt;区间元素出现次数&lt;/h2&gt;
&lt;p&gt;给定一个序列，判断指定区间每个元素出现次数是否都是偶数次（2的倍数），可以用异或前缀和实现，判断异或前缀和是否是0即可。那如果要判断每个元素出现次数是否都是3或3的倍数呢？或者出现次数都是$k$或$k$的倍数呢？&lt;/p&gt;
&lt;p&gt;考虑实现$k$进制异或，异或其实就是不带进位的加法，因此模拟一遍即可，只不过比起二进制要多一个$\log$的常数。&lt;/p&gt;
&lt;p&gt;更多问题中，要求的不是$k$或$k$的倍数，而是要求判断区间元素是否精准是$k$次。精准是$k$次，我们只需要解决一个问题：维护区间元素出现次数不多于$k$的区间即可。可以从左到右以1的步长移动右端点，同时记录以当前右端点，区间元素出现次数不多于$k$的区间左端点极限，同时用个&lt;code&gt;map&lt;/code&gt;维护一下相同异或前缀和即可。&lt;/p&gt;
&lt;p&gt;简而言之，也就是双指针加异或前缀和，利用$k$进制异或的性质来维护双指针区间内有多少个满足条件的端点。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/1418/G&quot;&gt;codeforces - 1418 - G&lt;/a&gt;（出现3次）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个由 $n$ 个正整数构造的数组 $a$. 我们说子数组 $a[l..r]$ 也就是 $[a_l, a_{l + 1}, \dots, a_r]$ ($1 \le l \le r \le n$) 是好的，当且仅当子数组中的每个元素都恰好出现3次。例如 $[1, 2, 2, 2, 1, 1, 2, 2, 2]$ 有3个好的子数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$a[1..6] = [1, 2, 2, 2, 1, 1]$;&lt;/li&gt;
&lt;li&gt;$a[2..4] = [2, 2, 2]$;&lt;/li&gt;
&lt;li&gt;$a[7..9] = [2, 2, 2]$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计算给定的数组 $a$ 的好的子数组的数目。&lt;/p&gt;
&lt;p&gt;数据规模：$1\leq n\leq 5e5$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#endif

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=5e5;

int n;
int a[maxn+5];

#define KNUM (3)    // K=3进制
#define KLEN (64)   // 用K进制64位数表示
typedef array&amp;lt;int, KLEN&amp;gt; Num;
Num operator ^(const Num &amp;amp;a, const Num &amp;amp;b){
    Num c;
    for (int i=0;i&amp;lt;KLEN;i++){
        c[i]=a[i]+b[i];
        if (c[i]&amp;gt;=KNUM)
            c[i]-=KNUM;
    }
    return c;
}
mt19937 rnd(time(0)); 
Num gen(void){  // 生成一个随机的K进制数
    Num ret;
    for (int i=0;i&amp;lt;KLEN;i++)
        ret[i]=rnd()%KNUM;
    return ret;
}

struct XORHashing {
    vector&amp;lt;Num&amp;gt; hash;
    // 初始化哈希[0,n]
    XORHashing(int n){
        hash.resize(n+1);
        for (int i=0;i&amp;lt;=n;i++)
            hash[i]=gen();
    }
    Num get_hash(int x){
        return hash[x];
    }
};

void solve(void){
	cin&amp;gt;&amp;gt;n;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    XORHashing xh(n);

    vector&amp;lt;vector&amp;lt;int&amp;gt; &amp;gt; pos(n+1);  // 存储每个数字出现的位置
    vector&amp;lt;Num&amp;gt; pre(n+1); // 前缀和，初始只有0
    map&amp;lt;Num,int&amp;gt; cnt;   // 存储每个前缀和出现的次数
    int l=0;
    ll ans=0;
    cnt[pre[0]]=1;
    for (int i=1;i&amp;lt;=n;i++){
        pre[i]=pre[i-1]^xh.get_hash(a[i]);
        pos[a[i]].push_back(i);
        if (pos[a[i]].size()&amp;gt;=(KNUM+1)){
            int target=pos[a[i]][pos[a[i]].size()-(KNUM+1)];
            while (l&amp;lt;target)
                --cnt[pre[l++]];
        }
        ans+=cnt[pre[i]];
        ++cnt[pre[i]];
    }
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
//	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://ac.nowcoder.com/acm/contest/81602/D&quot;&gt;2024牛客暑期多校7 - D&lt;/a&gt;（出现$  k$次）&lt;/h3&gt;
&lt;p&gt;题目和前面类似，但是由$3$次改成了任意$k$次，同时数据规模$a_i$也从$[1,n]$变成了$[0,1e9]$。&lt;/p&gt;
&lt;p&gt;对于改成任意$k$次，只需要将&lt;code&gt;KNUM&lt;/code&gt;进制改成变量即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// #define KNUM (3)    // K=3进制
#define KLEN (64)   // 用K进制64位数表示
int KNUM=3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于$a_i$的数据规模扩大，只需要离散化即可，这里可以预处理，把每个数都映射回$[1,n]$，因为我们不关心具体的值，只关心是否相等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 对x数组做离散化重映射 */
/**
例如，输入 1 1 4 5 1 4
修改为 1 1 2 3 1 2
*/
void prepare(int x[], int len){
    int b[len];
    memcpy(b,x,sizeof(b));
    sort(b,b+len);
    unordered_map&amp;lt;int,int&amp;gt; tag;
    int cnt=1;
    tag.insert(make_pair(b[0],1));
    for (int i=1;i&amp;lt;len;i++)
        if (b[i]!=b[i-1])
            tag.insert(make_pair(b[i],++cnt));
    for (int i=0;i&amp;lt;len;i++)
        x[i]=tag[x[i]];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#endif

typedef unsigned long long ull;
typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; P;
const int maxn=5e5;

int n;
int a[maxn+5];

// #define KNUM (3)    // K=3进制
#define KLEN (64)   // 用K进制64位数表示
int KNUM=3;
typedef array&amp;lt;int, KLEN&amp;gt; Num;
Num operator ^(const Num &amp;amp;a, const Num &amp;amp;b){
    Num c;
    for (int i=0;i&amp;lt;KLEN;i++){
        c[i]=a[i]+b[i];
        if (c[i]&amp;gt;=KNUM)
            c[i]-=KNUM;
    }
    return c;
}
mt19937 rnd(time(0)); 
Num gen(void){  // 生成一个随机的K进制数
    Num ret;
    for (int i=0;i&amp;lt;KLEN;i++)
        ret[i]=rnd()%KNUM;
    return ret;
}

struct XORHashing {
    vector&amp;lt;Num&amp;gt; hash;
    // 初始化哈希[0,n]
    XORHashing(int n){
        hash.resize(n+1);
        for (int i=0;i&amp;lt;=n;i++)
            hash[i]=gen();
    }
    Num get_hash(int x){
        return hash[x];
    }
};

void prepare(int x[], int len){
    int b[len];
    memcpy(b,x,sizeof(b));
    sort(b,b+len);
    unordered_map&amp;lt;int,int&amp;gt; tag;
    int cnt=1;
    tag.insert(make_pair(b[0],1));
    for (int i=1;i&amp;lt;len;i++)
        if (b[i]!=b[i-1])
            tag.insert(make_pair(b[i],++cnt));
    for (int i=0;i&amp;lt;len;i++)
        x[i]=tag[x[i]];
}

void solve(void){
	cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;KNUM;
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    prepare(a+1,n);
    XORHashing xh(n);

    vector&amp;lt;vector&amp;lt;int&amp;gt; &amp;gt; pos(n+1);  // 存储每个数字出现的位置
    vector&amp;lt;Num&amp;gt; pre(n+1); // 前缀和，初始只有0
    map&amp;lt;Num,int&amp;gt; cnt;   // 存储每个前缀和出现的次数
    int l=0;
    ll ans=0;
    cnt[pre[0]]=1;
    for (int i=1;i&amp;lt;=n;i++){
        pre[i]=pre[i-1]^xh.get_hash(a[i]);
        pos[a[i]].push_back(i);
        if (pos[a[i]].size()&amp;gt;=(KNUM+1)){
            int target=pos[a[i]][pos[a[i]].size()-(KNUM+1)];
            while (l&amp;lt;target)
                --cnt[pre[l++]];
        }
        ans+=cnt[pre[i]];
        ++cnt[pre[i]];
    }
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);
	int t=1;
	cin&amp;gt;&amp;gt;t;
	while (t--)
		solve();
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他题（TODO）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] https://codeforces.com/problemset/problem/1175/F：查询是否只出现一次&lt;/li&gt;
&lt;li&gt;[ ] https://codeforces.com/problemset/problem/869/E&lt;/li&gt;
&lt;li&gt;[ ] https://codeforces.com/problemset/problem/1418/G&lt;/li&gt;
&lt;li&gt;[ ] https://codeforces.com/contest/895/problem/C&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/notonlysuccess/article/details/130959107&quot;&gt;【算法笔记】杂项算法——异或哈希（xor-hashing）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/blog/entry/85900&quot;&gt;Codeforces - XOR Hashing [TUTORIAL] &lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>ESP32实现物联网语音助手</title><link>https://fuwari.vercel.app/posts/embed/esp32-voice-helper/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/esp32-voice-helper/</guid><pubDate>Wed, 07 Aug 2024 23:34:32 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h1&gt;ESP32联网语音助手&lt;/h1&gt;
&lt;p&gt;在做的项目需要一个联网语音部分，联网接入大模型或者其他的API，同时给OpenMV发信号，单独做出来好了（）。&lt;/p&gt;
&lt;h2&gt;ESP32回顾&lt;/h2&gt;
&lt;p&gt;先来点个灯回顾一下ESP32的基础操作。Arduino IDE选&lt;code&gt;ESP32 Dev Module&lt;/code&gt;。烧录一个点灯程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint8_t led_pin = 2;

void setup()
{
    pinMode(led_pin, OUTPUT);
}

void loop()
{
    digitalWrite(led_pin, HIGH);
    delay(500);
    digitalWrite(led_pin, LOW);
    delay(500);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一. 语音模块&lt;/h2&gt;
&lt;h3&gt;例程&lt;/h3&gt;
&lt;p&gt;选了语音模块是SYN6288。&lt;/p&gt;
&lt;p&gt;烧录示例代码，烧录时不能接线，猜测是串口的原因。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void speech(){
  unsigned char i = 0;
  unsigned char head[56];

  head[0] = 0xFD;
  head[1] = 0x00;
  head[2] = 0x35;
  head[3] = 0x01;
  head[4] = 0x00;
  head[5] = 0x5B;
  head[6] = 0x6D;
  head[7] = 0x34;
  head[8] = 0x5D;
  head[9] = 0x5B;
  head[10] = 0x76;
  head[11] = 0x31;
  head[12] = 0x36;
  head[13] = 0x5D;
  head[14] = 0x5B;
  head[15] = 0x74;
  head[16] = 0x35;
  head[17] = 0x5D;
  head[18] = 0xBB;
  head[19] = 0xB6;
  head[20] = 0xD3;
  head[21] = 0xAD;
  head[22] = 0xCA;
  head[23] = 0xB9;
  head[24] = 0xD3;
  head[25] = 0xC3;
  head[26] = 0xC2;
  head[27] = 0xCC;
  head[28] = 0xC9;
  head[29] = 0xEE;
  head[30] = 0xC6;
  head[31] = 0xEC;
  head[32] = 0xBD;
  head[33] = 0xA2;
  head[34] = 0xB5;
  head[35] = 0xEA;
  head[36] = 0x53;
  head[37] = 0x59;
  head[38] = 0x4E;
  head[39] = 0x36;
  head[40] = 0x32;
  head[41] = 0x38;
  head[42] = 0x38;
  head[43] = 0xD3;
  head[44] = 0xEF;
  head[45] = 0xD2;
  head[46] = 0xF4;
  head[47] = 0xBA;
  head[48] = 0xCF;
  head[49] = 0xB3;
  head[50] = 0xC9;
  head[51] = 0xC4;
  head[52] = 0xA3;
  head[53] = 0xBF;
  head[54] = 0xE9;
  head[55] = 0x91;

  for(i=0; i&amp;lt;56; i++){
    Serial.write(head[i]);
  }
}


void setup() {
  Serial.begin(9600);
}

void loop() {
  speech();
  delay(10000);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;烧录完后接线，就是正常的接&lt;code&gt;VCC (3.3V)&lt;/code&gt;和&lt;code&gt;GND&lt;/code&gt;，串口选用&lt;code&gt;UART0&lt;/code&gt;的&lt;code&gt;TX&lt;/code&gt;和&lt;code&gt;RX&lt;/code&gt;。会有声音出来。&lt;/p&gt;
&lt;h3&gt;消息协议&lt;/h3&gt;
&lt;p&gt;我希望实现一个输入文字能直接输出语音的效果，因此SYN6288模块例程还不够，需要解析一下这个协议是什么。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.elecfans.com/web2/M00/A4/22/poYBAGRbf8uAeca5AABUKrwRK3w378.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从文档的协议可以看出来，前三个字节可以不用管，第四个字节也不用管，在播放指令的时候命令字节都是&lt;code&gt;0x01&lt;/code&gt;，第六个字节，因为我不加背景音乐，所以只关心最末3位，也就是格式控制位。在播放命令的时候，如果是GB2312格式的数据，就是&lt;code&gt;0x00&lt;/code&gt;，测试了一下这个格式比较正常，或者GBK也可以，unicode有一点问题，我懒得解决了，就这样吧。&lt;/p&gt;
&lt;p&gt;（arduino ide暂时没找到比较快的改编码格式方案，因此我选择gbk也就是说只能在vscode中写，ide只作为一个烧录工具。当然也可以选unicode格式，格式命令改成&lt;code&gt;0x03&lt;/code&gt;就行。&lt;/p&gt;
&lt;p&gt;接下来是数据内容。中文一个字符由2个字节表示。下面是一个传入字符串，输出消息格式的代码，测试了一下，和SYN6288厂商提供的上位机的转码是一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;
#include &quot;stdio.h&quot;

void speech(char str[])
{
    static char data[256];
    static uint8_t len, str_len;

    data[0] = 0xFD; // 固定帧头
    data[3] = 0x01; // 播放指令
    data[4] = 0x00; // 格式gb2312
//    data[4] = 0x03; // 格式unicode

    str_len = strlen(str);
    memcpy(data + 5, str, str_len);

    data[1] = 0x00; // 数据长度
    data[2] = str_len + 3;

    len = str_len + 5;
    data[len] = 0;
    for (uint8_t i = 0; i &amp;lt; len; i++)
        data[len] ^= data[i];
    len++;

    for (uint8_t i = 0; i &amp;lt; len; i++)
        printf(&quot;0x%02X &quot;, (uint8_t)data[i]);
}

int main()
{
    char str[] = &quot;你好&quot;;
    speech(str);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;语音实现&lt;/h3&gt;
&lt;p&gt;然后可以开始写实际的代码了，大致如下，烧录时记得要把模块拔了。（这设计真有点有了了，看了下这个模块的VCC和GND还很容易接反。）&lt;/p&gt;
&lt;p&gt;第一遍写的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

uint8_t led_pin = 2;

void setup()
{
    pinMode(led_pin, OUTPUT);
    Serial.begin(9600);
}

void speech(const char str[])
{
    static char data[256];
    static uint8_t len, str_len;

    data[0] = 0xFD; // 锟教讹拷帧头
    data[3] = 0x01; // 锟斤拷锟斤拷指锟斤拷
    data[4] = 0x00; // 锟斤拷式gb2312
                    //    data[4] = 0x03; // 锟斤拷式unicode

    str_len = strlen(str);
    memcpy(data + 5, str, str_len);

    data[1] = 0x00; // 锟斤拷锟捷筹拷锟斤拷
    data[2] = str_len + 3;

    len = str_len + 5;
    data[len] = 0;
    for (uint8_t i = 0; i &amp;lt; len; i++)
        data[len] ^= data[i];
    len++;

    for (uint8_t i = 0; i &amp;lt; len; i++)
        Serial.write(data[i]);
}

void loop()
{
    speech(&quot;你好！&quot;);
    digitalWrite(led_pin, HIGH);
    delay(1000);
    digitalWrite(led_pin, LOW);
    delay(1000);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;状态查询&lt;/h3&gt;
&lt;p&gt;但是很容易注意到，这模块在说话的时候又收到指令时，会停止当前说话，因此要要检查一下它话说完了吗。没有命令参数和数据内容，因此数据区长度直接就是2.&lt;/p&gt;
&lt;p&gt;可以类似这样查询（下面这段代码没测试，因为有更好的方法）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool check_speaking(){
    static char data[256];
    static uint8_t len;

    data[0] = 0xFD; // Head
    data[1] = 0x00, data[2] = 0x02; // len
    data[3] = 0x21; // Check Speaking
    data[4] = 0xDE;
    len = 5;
    
    while (Serial.available() == 0);
    String receivedData = Serial.readString();
    if (receivedData[0] == 0x4F)
        return true;
    else
        return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是一直进行串口通信肯定是不优的，我们还得分个中断来检查收到的内容。SYN6288支持硬件查询，也就是通过查询引脚BY的高低电平来判断。BY引脚是低电平的时候，说明空闲。&lt;/p&gt;
&lt;p&gt;通过判断空闲来连续发送的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

uint8_t led_pin = 2;
uint8_t busy_pin = 12;

bool check_busy()
{
    return digitalRead(busy_pin);
}

void setup()
{
    pinMode(led_pin, OUTPUT);

    pinMode(busy_pin, INPUT);
    Serial.begin(9600);
}

void speech(const char str[])
{
    static char data[256];
    static uint8_t len, str_len;

    data[0] = 0xFD; // Head
    data[3] = 0x01; // Speak
    data[4] = 0x00; // gb2312
                    //    data[4] = 0x03; // unicode

    str_len = strlen(str);
    memcpy(data + 5, str, str_len);

    data[1] = 0x00;
    data[2] = str_len + 3;

    len = str_len + 5;
    data[len] = 0;
    for (uint8_t i = 0; i &amp;lt; len; i++)
        data[len] ^= data[i];
    len++;

    for (uint8_t i = 0; i &amp;lt; len; i++)
        Serial.write(data[i]);
}

void loop()
{
    if (check_busy() == false)
        speech(&quot;你好！&quot;);
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更换串口&lt;/h3&gt;
&lt;p&gt;串口0一般我们是用来调试的，所以要更换到其他串口。我换到ESP32的第二个串口，也就是&lt;code&gt;RX&lt;/code&gt;、&lt;code&gt;TX&lt;/code&gt;分别为&lt;code&gt;16&lt;/code&gt;、&lt;code&gt;17&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码和接线都改一下就行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

uint8_t led_pin = 2;
uint8_t busy_pin = 12;

HardwareSerial voice_serial(1);  // 定义串口

bool check_busy()
{
    return digitalRead(busy_pin);
}

void setup()
{
    pinMode(led_pin, OUTPUT);
    pinMode(busy_pin, INPUT);
    voice_serial.begin(9600, SERIAL_8N1, 16, 17);  // 初始化串口，波特率为9600，引脚16为TX，17为RX
}

void speech(const char str[])
{
    static char data[256];
    static uint8_t len, str_len;

    data[0] = 0xFD; // Head
    data[3] = 0x01; // Speak
    data[4] = 0x00; // gb2312
                    //    data[4] = 0x03; // unicode

    str_len = strlen(str);
    memcpy(data + 5, str, str_len);

    data[1] = 0x00;
    data[2] = str_len + 3;

    len = str_len + 5;
    data[len] = 0;
    for (uint8_t i = 0; i &amp;lt; len; i++)
        data[len] ^= data[i];
    len++;

    for (uint8_t i = 0; i &amp;lt; len; i++)
        voice_serial.write(data[i]);  // 使用新的串口发送数据
}

void loop()
{
    if (check_busy() == false)
        speech(&quot;你好！&quot;);
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模块封装&lt;/h3&gt;
&lt;p&gt;接着把这玩意封装成c++的类，我个人感觉这样看着比较好看一点（）。特别是ESP32支持这个，就比其他只支持C的单片机更爽写ovo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

class VoiceModule
{
private:
    HardwareSerial &amp;amp;voice_serial;
    uint8_t busy_pin;

public:
    // 构造函数，传入串口和busy引脚的序号
    VoiceModule(HardwareSerial &amp;amp;serial, uint8_t busy_pin)
        : voice_serial(serial), busy_pin(busy_pin) {}

    // 初始化函数，设置引脚模式和串口参数
    void begin(uint32_t baudRate, uint8_t txPin, uint8_t rxPin)
    {
        pinMode(busy_pin, INPUT);
        voice_serial.begin(baudRate, SERIAL_8N1, txPin, rxPin);
    }

    // 检查忙碌状态
    bool isBusy()
    {
        return digitalRead(busy_pin);
    }

    // 发送语音数据
    void speech(const char str[])
    {
        static char data[256];
        static uint8_t len, str_len;

        data[0] = 0xFD; // Head
        data[3] = 0x01; // Speak
        data[4] = 0x00; // gb2312
                        //    data[4] = 0x03; // unicode

        str_len = strlen(str);
        memcpy(data + 5, str, str_len);

        data[1] = 0x00;
        data[2] = str_len + 3;

        len = str_len + 5;
        data[len] = 0;
        for (uint8_t i = 0; i &amp;lt; len; i++)
            data[len] ^= data[i];
        len++;

        for (uint8_t i = 0; i &amp;lt; len; i++)
            voice_serial.write(data[i]);
    }
};

uint8_t led_pin = 2;

HardwareSerial voiceSerial(1);      // 定义串口实例
VoiceModule voice(voiceSerial, 12); // 创建语音模块实例，busy_pin为12

void setup()
{
    pinMode(led_pin, OUTPUT);
    voice.begin(9600, 16, 17); // 初始化语音模块，波特率为9600，引脚16为TX，17为RX
}

void loop()
{
    if (voice.isBusy() == false)
    {
        voice.speech(&quot;你好！&quot;);
    }
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二. 联网部分&lt;/h2&gt;
&lt;p&gt;计划采用ESP32连接服务器，然后接大模型。那首先就要实现一个网络接口，能够实现传入一个中文字符串后，能发声。&lt;/p&gt;
&lt;p&gt;这里我计划使用MQTT消息协议，选择的原因一个是比较好写，一个是快啊，而且好调试。&lt;/p&gt;
&lt;p&gt;首先用我自己的MQTT服务器，确定两个东西：本地WIFI和服务器IP、MQTT框架运行的端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SSID = &quot;foresp1&quot;  # Network SSID
KEY = &quot;20040724&quot;  # Network key

MQTTClient(&quot;openmv&quot;, &quot;113.45.173.169&quot;, port=1883)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MQTT服务器测试&lt;/h3&gt;
&lt;p&gt;先自己写个python测试一下MQTT的收发是否正常，我把服务器和订阅的信息都存在和python同目录的&lt;code&gt;config.yaml&lt;/code&gt;文件下，也就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  address: &quot;113.45.173.169&quot;             # MQTT服务器地址
  port: 1883                            # MQTT服务器端口
  topics:                               # 可订阅主题
    - &quot;openmv/voice&quot;

client:
  name: &quot;aicoach&quot;                         # 监听客户端名称
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送的文件是&lt;code&gt;sender.py&lt;/code&gt;，测试接收的文件是&lt;code&gt;listener.py&lt;/code&gt;，这两个在不同的终端同时运行，就可以发送然后测试是否收到了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;&apos;&apos;
Author: wlaten
Date: 2024-07-05 20:49:29
LastEditTime: 2024-07-23 01:49:12
Discription: file content
&apos;&apos;&apos;
import os
import paho.mqtt.client as mqtt
import yaml

&quot;&quot;&quot;
listener.py
测试MQTT监听程序
&quot;&quot;&quot;

current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(current_dir, &apos;config.yaml&apos;)

with open(&apos;config.yaml&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)
MQTT_SERVER = config[&apos;server&apos;][&apos;address&apos;] 
MQTT_PORT = config[&apos;server&apos;][&apos;port&apos;]
MQTT_TOPIC = config[&apos;server&apos;][&apos;topics&apos;]

def on_message(client, userdata, msg):
    message = msg.payload.decode()
    topic = msg.topic
    print(f&quot;Received message &apos;{message}&apos; on topic &apos;{topic}&apos;&quot;)
    

def on_connect(client, userdata, flags, rc):
    print(&quot;Connected with result code &quot;+str(rc))
    for topic in MQTT_TOPIC:
        client.subscribe(topic)
    
if __name__ == &apos;__main__&apos;:
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message

    client.username_pw_set(config[&apos;client&apos;][&apos;name&apos;])
    client.connect(MQTT_SERVER, MQTT_PORT, 60)

    client.loop_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
    sender.py
    模拟客户端，测试MQTT服务器收发消息
&quot;&quot;&quot;

import os
import paho.mqtt.client as mqtt
import yaml

# 获取当前脚本文件的绝对路径，然后获取同目录下的config.yaml文件的路径
current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(current_dir, &apos;config.yaml&apos;)

with open(config_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
    config = yaml.safe_load(file)
broker_address = config[&apos;server&apos;][&apos;address&apos;] 
broker_port = config[&apos;server&apos;][&apos;port&apos;]

def on_connect(client, userdata, flags, rc):
    print(f&quot;Connected with result code {rc}&quot;)

client = mqtt.Client()  
client.on_connect = on_connect 
client.connect(broker_address, broker_port, 60)

client.loop_start()

while True:
    topic = input(&quot;请输入主题: &quot;)
    message = input(&quot;请输入消息内容: &quot;)
    client.publish(topic, message)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试了一下我的MQTT服务器运行正常，好可以开始写ESP32了。&lt;/p&gt;
&lt;h3&gt;ESP32连接MQTT&lt;/h3&gt;
&lt;p&gt;先写一个ESP32连接WIFI的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;
#include &amp;lt;WiFi.h&amp;gt;

#include &quot;VoiceModule.h&quot;		// 把前面的类封装进来

uint8_t led_pin = 2;

HardwareSerial voiceSerial(1);      // 定义串口实例
VoiceModule voice(voiceSerial, 12); // 创建语音模块实例，busy_pin为12

void network_init()
{
    const char *ssid = &quot;foresp1&quot;;
    const char *password = &quot;20040724&quot;;

    Serial.println();
    Serial.print(&quot;正在连接到 &quot;);
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(&quot;.&quot;);
    }

    Serial.println(&quot;&quot;);
    Serial.println(&quot;WiFi 已连接&quot;);
    Serial.println(&quot;IP 地址: &quot;);
    Serial.println(WiFi.localIP());
}

void setup()
{
    Serial.begin(115200);	// 初始化调试串口
    Serial.println(&quot;Serial start...&quot;);
    
    network_init();			// 初始化网络

    pinMode(led_pin, OUTPUT);
    voice.begin(9600, 16, 17); // 初始化语音模块，波特率为9600，引脚16为TX，17为RX
}

void loop()
{
    if (voice.isBusy() == false)
    {
        voice.speech(&quot;你好！&quot;);
    }
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后写连接MQTT服务器的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const char *mqtt_server = &quot;113.45.173.169&quot;;
const int mqtt_port = 1883;
const char *mqtt_user = &quot;voice&quot;;
const char *mqtt_topic = &quot;openmv/test&quot;;

WiFiClient espClient;
PubSubClient client(espClient);

void network_init()
{
    /* 初始化WIFI */
    const char *ssid = &quot;foresp1&quot;;
    const char *password = &quot;20040724&quot;;

    Serial.println();
    Serial.print(&quot;正在连接到 &quot;);
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(&quot;.&quot;);
    }

    Serial.println(&quot;&quot;);
    Serial.println(&quot;WiFi 已连接&quot;);
    Serial.println(&quot;IP 地址: &quot;);
    Serial.println(WiFi.localIP());

    /* 初始化MQTT */
    client.setServer(mqtt_server, mqtt_port);
    //    client.setCallback(mqttCallback);
    Serial.println(&quot;MQTT连接中...&quot;);
    while (!client.connected())
    {
        if (client.connect(mqtt_user))
            break;
        else
        {
            delay(100);
            Serial.print(&quot;.&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在服务器上查询，可以看见已经连接成功了。&lt;/p&gt;
&lt;p&gt;但是现在会发现，尽管连接成功了，但是过一会就会断开，这是因为没有发送心跳包保持连接，所以要在&lt;code&gt;loop&lt;/code&gt;函数里面持续调用&lt;code&gt;client.loop()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;或者为了进一步保持连接稳定，加入掉线重连接功能，完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

#include &amp;lt;WiFi.h&amp;gt;
#include &amp;lt;PubSubClient.h&amp;gt;

#include &quot;VoiceModule.h&quot;

uint8_t led_pin = 2;

HardwareSerial voiceSerial(1);      // 定义串口实例
VoiceModule voice(voiceSerial, 12); // 创建语音模块实例，busy_pin为12

const char *mqtt_server = &quot;113.45.173.169&quot;;
const int mqtt_port = 1883;
const char *mqtt_user = &quot;voice&quot;;
const char *mqtt_topic = &quot;openmv/test&quot;;

WiFiClient espClient;
PubSubClient client(espClient);

void mqtt_connect(){
    Serial.println(&quot;MQTT连接中...&quot;);
    while (!client.connected())
    {
        if (client.connect(mqtt_user))
            break;
        else
        {
            delay(100);
            Serial.print(&quot;.&quot;);
        }
    }
    Serial.println(&quot;MQTT已连接&quot;);
}

void network_init()
{
    /* 初始化WIFI */
    const char *ssid = &quot;foresp1&quot;;
    const char *password = &quot;20040724&quot;;

    Serial.println();
    Serial.print(&quot;正在连接到 &quot;);
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(&quot;.&quot;);
    }

    Serial.println(&quot;&quot;);
    Serial.println(&quot;WiFi 已连接&quot;);
    Serial.println(&quot;IP 地址: &quot;);
    Serial.println(WiFi.localIP());

    /* 初始化MQTT */
    client.setServer(mqtt_server, mqtt_port);
    //    client.setCallback(mqttCallback);
    mqtt_connect();
}



void setup()
{
    Serial.begin(115200);
    Serial.println(&quot;Serial start...&quot;);
    network_init();

    pinMode(led_pin, OUTPUT);
    voice.begin(9600, 16, 17); // 初始化语音模块，波特率为9600，引脚16为TX，17为RX
}

void loop()
{
    client.loop();
    if (!client.connected())
        mqtt_connect();
    if (voice.isBusy() == false)
    {
        voice.speech(&quot;你好！&quot;);
    }
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ESP32发送MQTT消息&lt;/h3&gt;
&lt;p&gt;尝试用ESP32发送一条消息到&lt;code&gt;openmv/test&lt;/code&gt;这个主题上。只需要随便在哪里这样写一下就好：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	String message = &quot;Hello from ESP32 你好！&quot;;
    client.publish(&quot;openmv/test&quot;, message.c_str());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候我们前面的python监听程序要改一下，因为监听默认是utf8格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def on_message(client, userdata, msg):
    try:
        message = msg.payload.decode(&apos;utf-8&apos;)  # 尝试用UTF-8解码
    except UnicodeDecodeError:
        message = msg.payload.decode(&apos;gbk&apos;)  # 如果失败，改用GBK解码
    topic = msg.topic
    print(f&quot;Received message &apos;{message}&apos; on topic &apos;{topic}&apos;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以成功收到这样的消息了。&lt;/p&gt;
&lt;h3&gt;ESP32接收MQTT消息&lt;/h3&gt;
&lt;p&gt;MQTT首先要订阅，然后接收。&lt;/p&gt;
&lt;p&gt;首先确定订阅主题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const char *mqtt_topics_sub[] = {
    &quot;openmv/test&quot;,
    &quot;openmv/voice&quot;,
    &quot;sport_cmd&quot;};
const int num_topics = sizeof(topics) / sizeof(topics[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改连接函数，在连接中订阅：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void mqtt_connect()
{
    Serial.println(&quot;MQTT连接中...&quot;);
    while (!client.connected())
    {
        if (client.connect(mqtt_user))
            break;
        else
        {
            delay(100);
            Serial.print(&quot;.&quot;);
        }
    }
    for (int i = 0; i &amp;lt; topics_sub_cnt; i++)
    {
        client.subscribe(topics[i]);
        Serial.print(&quot;Subscribed to topic: &quot;);
        Serial.println(topics[i]);
    }
    Serial.println(&quot;MQTT已连接&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要设置回调函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client.setCallback(mqtt_callback);	// 加在network_init()里
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void mqtt_callback(char *topic, byte *payload, unsigned int len)
{
    Serial.print(&quot;Message arrived on topic: &quot;);
    Serial.print(topic);
    Serial.print(&quot;. Message: &quot;);

    String messageTemp;

    for (int i = 0; i &amp;lt; len; i++)
        messageTemp += (char)payload[i];
    Serial.println(messageTemp);

    // 处理消息...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以正常收发了，这里还需要修改&lt;code&gt;sender.py&lt;/code&gt;文件来测试，保证发送是gbk格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while True:
    topic = input(&quot;请输入主题: &quot;)
    message = input(&quot;请输入消息内容: &quot;).encode(&apos;gbk&apos;)	// 编码gbk
    client.publish(topic, message)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现现在是可以正常收发的。&lt;/p&gt;
&lt;p&gt;现在整套代码基本实现了通过mqtt协议，网络传入中文字符串，语音模块发声。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

#include &amp;lt;WiFi.h&amp;gt;
#include &amp;lt;PubSubClient.h&amp;gt;

#include &quot;VoiceModule.h&quot;

uint8_t led_pin = 2;

HardwareSerial voiceSerial(1);      // 定义串口实例
VoiceModule voice(voiceSerial, 12); // 创建语音模块实例，busy_pin为12

const char *mqtt_server = &quot;113.45.173.169&quot;;
const int mqtt_port = 1883;
const char *mqtt_user = &quot;voice&quot;;
const char *mqtt_topic = &quot;openmv/test&quot;;

WiFiClient espClient;
PubSubClient client(espClient);

const char *mqtt_topics_sub[] = {
    &quot;openmv/test&quot;,
    &quot;openmv/voice&quot;,
    &quot;sport_cmd&quot;};
const int topics_sub_cnt = sizeof(mqtt_topics_sub) / sizeof(mqtt_topics_sub[0]);

void mqtt_connect()
{
    Serial.println(&quot;MQTT连接中...&quot;);
    while (!client.connected())
    {
        if (client.connect(mqtt_user))
            break;
        else
        {
            delay(100);
            Serial.print(&quot;.&quot;);
        }
    }
    for (int i = 0; i &amp;lt; topics_sub_cnt; i++)
    {
        client.subscribe(mqtt_topics_sub[i]);
        Serial.print(&quot;Subscribed to topic: &quot;);
        Serial.println(mqtt_topics_sub[i]);
    }
    Serial.println(&quot;MQTT已连接&quot;);
}

void mqtt_callback(char *topic, byte *payload, unsigned int len)
{
    Serial.print(&quot;Message arrived on topic: &quot;);
    Serial.print(topic);
    Serial.print(&quot;. Message: &quot;);

    String rec;
    for (int i = 0; i &amp;lt; len; i++)
        rec += (char)payload[i];
    Serial.println(rec);

    if (strcmp(topic, &quot;openmv/voice&quot;) == 0){
        if (voice.isBusy() == false)
            voice.speech(rec.c_str());
    }
}

void network_init()
{
    /* 初始化WIFI */
    const char *ssid = &quot;foresp1&quot;;
    const char *password = &quot;20040724&quot;;

    Serial.println();
    Serial.print(&quot;正在连接到 &quot;);
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(&quot;.&quot;);
    }

    Serial.println(&quot;&quot;);
    Serial.println(&quot;WiFi 已连接&quot;);
    Serial.println(&quot;IP 地址: &quot;);
    Serial.println(WiFi.localIP());

    /* 初始化MQTT */
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(mqtt_callback);
    mqtt_connect();
}

void setup()
{
    Serial.begin(115200);
    Serial.println(&quot;Serial start...&quot;);
    network_init();

    pinMode(led_pin, OUTPUT);
    voice.begin(9600, 16, 17); // 初始化语音模块，波特率为9600，引脚16为TX，17为RX
}

void loop()
{
    client.loop();
    if (!client.connected())
        mqtt_connect();
    // if (voice.isBusy() == false)
    // {
    //     voice.speech(&quot;你好！&quot;);
    // }
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;队列实现缓冲区&lt;/h3&gt;
&lt;p&gt;可以发现还是有一个问题，就是因为这里我没有实现打断说话功能，因此正在说话时收到的消息就被丢弃了。&lt;/p&gt;
&lt;p&gt;可以用一个&lt;code&gt;queue&lt;/code&gt;实现缓冲区（c++真爽写吧，换其他只支持c的单片机就要写好多）。此外，针对字节上限，也要做一个分割。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdint.h&quot;
#include &quot;string.h&quot;

#include &quot;queue&quot;
using namespace std;

#include &amp;lt;WiFi.h&amp;gt;
#include &amp;lt;PubSubClient.h&amp;gt;

#include &quot;VoiceModule.h&quot;

uint8_t led_pin = 2;

HardwareSerial voiceSerial(1);      // 定义串口实例
VoiceModule voice(voiceSerial, 12); // 创建语音模块实例，busy_pin为12

const char *mqtt_server = &quot;113.45.173.169&quot;;
const int mqtt_port = 1883;
const char *mqtt_user = &quot;voice&quot;;
const char *mqtt_topic = &quot;openmv/test&quot;;

WiFiClient espClient;
PubSubClient client(espClient);

const char *mqtt_topics_sub[] = {
    &quot;openmv/test&quot;,
    &quot;openmv/voice&quot;,
    &quot;sport_cmd&quot;};
const int topics_sub_cnt = sizeof(mqtt_topics_sub) / sizeof(mqtt_topics_sub[0]);

queue&amp;lt;String&amp;gt; voice_queue;  // 语音队列

void mqtt_connect()
{
    Serial.println(&quot;MQTT连接中...&quot;);
    while (!client.connected())
    {
        if (client.connect(mqtt_user))
            break;
        else
        {
            delay(100);
            Serial.print(&quot;.&quot;);
        }
    }
    for (int i = 0; i &amp;lt; topics_sub_cnt; i++)
    {
        client.subscribe(mqtt_topics_sub[i]);
        Serial.print(&quot;Subscribed to topic: &quot;);
        Serial.println(mqtt_topics_sub[i]);
    }
    Serial.println(&quot;MQTT已连接&quot;);
}

void mqtt_callback(char *topic, byte *payload, unsigned int len)
{
    Serial.print(&quot;Message arrived on topic: &quot;);
    Serial.print(topic);
    Serial.print(&quot;. Message: &quot;);

    String rec;
    for (int i = 0; i &amp;lt; len; i++)
        rec += (char)payload[i];
    Serial.println(rec);

    if (strcmp(topic, &quot;openmv/voice&quot;) == 0)
        voice_queue.push(rec);
}

void network_init()
{
    /* 初始化WIFI */
    const char *ssid = &quot;foresp1&quot;;
    const char *password = &quot;20040724&quot;;

    Serial.println();
    Serial.print(&quot;正在连接到 &quot;);
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(&quot;.&quot;);
    }

    Serial.println(&quot;&quot;);
    Serial.println(&quot;WiFi 已连接&quot;);
    Serial.println(&quot;IP 地址: &quot;);
    Serial.println(WiFi.localIP());

    /* 初始化MQTT */
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(mqtt_callback);
    mqtt_connect();
}

void setup()
{
    Serial.begin(115200);
    Serial.println(&quot;Serial start...&quot;);
    network_init();

    pinMode(led_pin, OUTPUT);
    voice.begin(9600, 16, 17); // 初始化语音模块，波特率为9600，引脚16为TX，17为RX
}

void loop()
{
    client.loop();
    if (!client.connected())
        mqtt_connect();
    if (voice.isBusy() == false)
    {
        if (!voice_queue.empty())
        {
            voice.speech(voice_queue.front().c_str());
            voice_queue.pop();
        }
    }
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>算法笔记：可持久化数据结构</title><link>https://fuwari.vercel.app/posts/acm/cp-note-persistent-ds/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/cp-note-persistent-ds/</guid><pubDate>Wed, 07 Aug 2024 19:06:36 GMT</pubDate><content:encoded>&lt;p&gt;狠补一下一些不太熟的东西。&lt;/p&gt;
&lt;h2&gt;可持久化线段树&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3919&quot;&gt;可持久化数组&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;要求维护一个数组，进行$m$次操作，每次操作都生成一个新的版本（版本编号从$1$到$m$）。操作如下：&lt;/p&gt;
&lt;p&gt;① 在某个给定历史版本上的基础上，该版本修改某个数的值。&lt;/p&gt;
&lt;p&gt;② 在某个给定历史版本的基础上，该版本不修改任何值，但要求查询第$x$个数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;朴素的实现肯定是爆时间空间的。考虑有什么方法维护，可持久化线段树可以维护。对于线段树，单次修改实际上只修改了一个长度为$\log n$的路径。那我们重新建树就行了，每次的新树，继承了前一个版本的树的剩余节点，新增的节点只有$\log n$个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class PersistentArray {
private:
    struct Node {
        T val;
        int l, r;
        int lson, rson;
    };

    vector&amp;lt;Node&amp;gt; node;
    vector&amp;lt;int&amp;gt; root;
    vector&amp;lt;T&amp;gt; data;

    /* 克隆当前节点，返回新节点的索引 */
    int clone(int base) {
        node.push_back(node[base]);
        return node.size() - 1;
    }

    /* 创建新节点，返回新节点的索引 */
    int create(T val, int l, int r, int lson, int rson) {
        node.push_back({val, l, r, lson, rson});
        return node.size() - 1;
    }

    /* 建立持久化数组的初始版本 */
    int build(int l, int r) {
        if (l == r)
            return create(data[l], l, r, -1, -1);
        else {
            int mid = (l + r) / 2;
            int lson = build(l, mid);
            int rson = build(mid + 1, r);
            return create(0, l, r, lson, rson);
        }
    }

    /* 修改持久化数组路径 */ 
    int revise_path(int u, int pos, const T val) {
        int p = clone(u);
        if (node[u].l == node[u].r) 
            node[p].val = val;
        else {
            int mid = (node[u].l + node[u].r) / 2;
            if (pos &amp;lt;= mid)
                node[p].lson = revise_path(node[u].lson, pos, val);
            else
                node[p].rson = revise_path(node[u].rson, pos, val);
        }
        return p;
    }

    /* 查询持久化数组的某个版本的值 */
    T ask_path(int u, int pos) {
        if (node[u].l == node[u].r)
            return node[u].val;
        else {
            int mid = (node[u].l + node[u].r) / 2;
            if (pos &amp;lt;= mid)
                return ask_path(node[u].lson, pos);
            else 
                return ask_path(node[u].rson, pos);
        }
    }

public:
    PersistentArray(vector&amp;lt;T&amp;gt;&amp;amp; initData) {
        data = initData;
        // 好像不加这个会RE
        node.reserve(32 * initData.size());
        root.reserve(initData.size()); // 预留空间
        root.push_back(build(0, data.size() - 1)); 
    }

    /* 修改指定版本的数组并返回新版本号 */
    int revise(int v, int pos, const T val) {
        root.push_back(revise_path(root[v], pos, val));
        return root.size() - 1;
    }

    /* 查询指定版本的数组的某个位置的值 */
    T ask(int v, int pos) {
        return ask_path(root[v], pos);
    }

    /* 复制一个版本（不修改）并返回版本号 */
    void copy(int v) {
        root.push_back(root[v]);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3834&quot;&gt;静态区间第$k$小问题&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;前置：权值线段树&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定 $n$ 个整数构成的序列 $a$，多次询问，将对于指定的闭区间 $[l, r]$ 查询其区间内的第 $k$ 小值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设我们对整个数组查询第$k$小，会怎么做？排序一遍，然后从小到大就是了。如果我们只需单次查询，那就给定了区间$[l,r]$后，同样的，对原先数组排序一遍，然后从小到大遍历排序后的全数组，对于每个元素检查原始位置是否在$[l,r]$中，直到找到$[l,r]$中的第$k$小。&lt;/p&gt;
&lt;p&gt;但是如果需要多次查询就比较麻烦了。我们单次查询的复杂度已经到了$O(n\log n)$，进行$q$次查询，复杂度会到$O(q\cdot n\log n)$，是比较坏的。因此，有一个想法就是，我们只做一次$[1,n]$的查询，能不能通过这一次的查询，预处理构造出一个数据结构，让我们能满足比较快的对$[l,r]$的查询？我们考虑记录一下$n$次插入的每次插入后的版本。很明显我们可以对这$n$次插入后的版本二分查询$[l,r]$中是否已经插入了$k$个元素，然后对恰好插入了$k$个元素的那个版本的线段树，查询$[l,r]$当前的最大值，也就是第$k$小元素。&lt;/p&gt;
&lt;p&gt;这个思路的问题是，我们要建立$n$个线段树，包爆空间的。&lt;/p&gt;
&lt;p&gt;如何解决？可以考虑复用数据结构。也就是说，考虑到每次创建一个新版本的线段树，和前一个版本的区别只在于插入了一个新的节点，也就是说线段树上有且仅有一个路径发生了更改。那我们创建新树的时候，只需要创建一个新的根节点，然后创建这个路径即可，其他的部分，我们直接指针指回上一个版本的节点即可。这就可以很大节省空间。&lt;/p&gt;
&lt;p&gt;也就是说，要建立一个可持久化的权值线段树，维护每个版本的区间的权值。对于从小到大的每个数$x$和它原先的位置$pos$，我们把它的位置$pos$插入这个权值线段树。&lt;/p&gt;
&lt;p&gt;这题也要加离散化会快一点，但是我用了个很快的快读能过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
const int maxn=1e5;

int n,q;

/* 可持久化权值线段树 */
template &amp;lt;typename T&amp;gt;
class CountSegmentTree{
private:
    struct Node {
        int cnt;          // 节点权值
        T l, r;       // [l,r]
        int lson, rson; // 左右儿子
    };

    struct Version{
        int root; // 根节点编号
        T revise_val;   // 这个版本与前一个版本相比修改的数
        int revise_cnt;   // 修改的权值数
    };
    vector&amp;lt;Node&amp;gt; node;          // 节点列表
    vector&amp;lt;Version&amp;gt; version;   // 版本信息存储

    int create(int val,T l,T r,int lson,int rson) {
        node.push_back({val,l,r,lson,rson});
        return node.size()-1;
    }
    int clone(int u){
        node.push_back(node[u]);
        return node.size()-1;
    }
    /* 建树，区间[l,r] */
    int build(T l,T r) {
        if (l==r)
            return create(0,l,r,-1,-1);
        else {
            T mid=(l+r)/2;
            return create(0,l,r,build(l,mid),build(mid+1,r));
        }
    }
    
    /* 对val值的所有路径节点修改x */
    void revise(T val,int x){
        int u=version.back().root;  // 默认从最后一个版本开始修改
        int *fa=nullptr;
        while (true){
            int v=clone(u);
            if (fa)
                *fa=v;
            else
                version.push_back(Version{v,val,x});
            node[v].cnt+=x;
            if (node[u].l==node[u].r)
                return ;
            T mid=(node[u].l+node[u].r)/2;
            u=(val&amp;lt;=mid)?node[u].lson:node[u].rson;
            fa=(val&amp;lt;=mid)?(&amp;amp;node[v].lson):(&amp;amp;node[v].rson);
        }
    }
    /* 查询区间权值 */
    int ask_range(int u,const T l,const T r){
        if (l&amp;lt;=node[u].l&amp;amp;&amp;amp;node[u].r&amp;lt;=r)
            return node[u].cnt;
        T mid=(node[u].l+node[u].r)/2;
        int ret=0;
        if (l&amp;lt;=mid)
            ret+=ask_range(node[u].lson,l,r);
        if (r&amp;gt;mid)
            ret+=ask_range(node[u].rson,l,r);
        return ret;
    }

public:
    CountSegmentTree(T l, T r){ 
        // 传入开的桶的范围，[L,R]
        node.reserve((r-l+5)*32);
        version.push_back(Version{build(l,r),0});
    }
    /* 插入值val，返回版本号 */
    int insert(T val){
        revise(val,1);
        return version.size()-1;
    }
    /* 删除值val，返回版本号 */
    void remove(T val){
        revise(val,-1);
        return version.size()-1;
    }
    /* 查询版本[L,R]的数的个数 */
    int ask_cnt(int ver, T l, T r){
        return ask_range(version[ver].root,l,r);
    }
    /* 查询版本[L,R]修改权值的数 */
    T ask_version_diff(int ver){
        return version[ver].revise_val;
    }
};

namespace fastio
{
    const int bufl=1&amp;lt;&amp;lt;16;
    const double base1[16]={1,1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,1e-7,1e-8,1e-9,1e-10,1e-11,1e-12,1e-13,1e-14,1e-15};
    const double base2[16]={1,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13,1e14,1e15};
    struct IN{
        FILE *IT;char ibuf[bufl],*is=ibuf,*it=ibuf;
        IN(){IT=stdin;}IN(char *a){IT=fopen(a,&quot;r&quot;);}
        inline char getChar(){if(is==it){it=(is=ibuf)+fread(ibuf,1,bufl,IT);if(is==it)return EOF;}return *is++;}
        template&amp;lt;typename Temp&amp;gt;inline void getInt(Temp &amp;amp;a){a=0;int b=0,c=getChar();while(c&amp;lt;48||c&amp;gt;57)b^=(c==45),c=getChar();while(c&amp;gt;=48&amp;amp;&amp;amp;c&amp;lt;=57)a=(a&amp;lt;&amp;lt;1)+(a&amp;lt;&amp;lt;3)+c-48,c=getChar();if(b)a=-a;}
        IN&amp;amp; operator&amp;gt;&amp;gt;(char &amp;amp;a){a=getChar();return *this;}
        IN&amp;amp; operator&amp;gt;&amp;gt;(char *a){do{*a=getChar();}while(*a&amp;lt;=32);while(*a&amp;gt;32)*++a=getChar();*a=0;return *this;}
        IN&amp;amp; operator&amp;gt;&amp;gt;(string &amp;amp;a){char b=getChar();while(b&amp;lt;=32)b=getChar();while(b&amp;gt;32)a+=b,b=getChar();return *this;}
        IN&amp;amp; operator&amp;gt;&amp;gt;(int &amp;amp;a){getInt(a);return *this;}
        IN&amp;amp; operator&amp;gt;&amp;gt;(long long &amp;amp;a){getInt(a);return *this;}
    };
    struct OUT{
        FILE *IT;char obuf[bufl],*os=obuf,*ot=obuf+bufl;int Eps;long double Acc;
        OUT(){IT=stdout,Eps=6,Acc=1e-6;}OUT(char *a){IT=fopen(a,&quot;w&quot;),Eps=6,Acc=1e-6;}
        inline void ChangEps(int x=6){Eps=x;}
        inline void flush(){fwrite(obuf,1,os-obuf,IT);os=obuf;}
        inline void putChar(int a){*os++=a;if(os==ot)flush();}
        template&amp;lt;typename Temp&amp;gt;inline void putInt(Temp a){if(a&amp;lt;0){putChar(45);a=-a;}if(a&amp;lt;10){putChar(a+48);return;}putInt(a/10);putChar(a%10+48);}
        OUT&amp;amp; operator&amp;lt;&amp;lt;(char a){putChar(a);return *this;}
        OUT&amp;amp; operator&amp;lt;&amp;lt;(char *a){while(*a&amp;gt;32)putChar(*a++);return *this;}
        OUT&amp;amp; operator&amp;lt;&amp;lt;(string a){for(auto c:a)putChar(c);return *this;}
        OUT&amp;amp; operator&amp;lt;&amp;lt;(int a){putInt(a);return *this;}
        OUT&amp;amp; operator&amp;lt;&amp;lt;(long long a){putInt(a);return *this;}
        ~OUT(){flush();}
    };
}
using fastio::IN;
using fastio::OUT;
IN fin;
OUT fout;

int main(){
//	freopen(&quot;P3834_9.in&quot;,&quot;r&quot;,stdin);
//	freopen(&quot;P3834_9.txt&quot;,&quot;w&quot;,stdout);
	fin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;q;
    typedef pair&amp;lt;int,int&amp;gt; P;
    vector&amp;lt;int&amp;gt; x(n+1);
    vector&amp;lt;P&amp;gt; a(n);
    for (int i=0;i&amp;lt;n;i++){
    	fin&amp;gt;&amp;gt;a[i].first;
        a[i].second=i+1;
        x[i+1]=a[i].first;
    }
    sort(a.begin(),a.end());
    CountSegmentTree&amp;lt;int&amp;gt; tree(1,n);
    for (P p: a)
        tree.insert(p.second);
    int L,R,k;
    int l,r,mid;
    while (q--){
    	fin&amp;gt;&amp;gt;L&amp;gt;&amp;gt;R&amp;gt;&amp;gt;k;
        l=0,r=n;
        while (r-l&amp;gt;1){
            mid=(l+r)&amp;gt;&amp;gt;1;
            if (tree.ask_cnt(mid,L,R)&amp;gt;=k)
                r=mid;
            else
                l=mid;
        }
        fout&amp;lt;&amp;lt;x[tree.ask_version_diff(r)]&amp;lt;&amp;lt;&apos;\n&apos;;
    }
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3402&quot;&gt;可持久化并查集&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定 $n$ 个集合，第 $i$ 个集合内初始状态下只有一个数，为 $i$。&lt;/p&gt;
&lt;p&gt;有 $m$ 次操作。操作分为 $3$ 种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 a b&lt;/code&gt; 合并 $a,b$ 所在集合；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 k&lt;/code&gt; 回到第 $k$ 次操作（执行三种操作中的任意一种都记为一次操作）之后的状态；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3 a b&lt;/code&gt; 询问 $a,b$ 是否属于同一集合，如果是则输出 $1$，否则输出 $0$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;普通的并查集进行修改后，主要的区别就是&lt;code&gt;fa&lt;/code&gt;数组。那就考虑对&lt;code&gt;fa&lt;/code&gt;数组进行可持久化，朴素的思路是建可持久化线段树。但是并查集通常带了路径压缩，没法撤销，因此要使用按深度合并的并查集，保证每次都是$\log$的。&lt;/p&gt;
&lt;p&gt;因此直接用可持久化线段树或者说可持久化数组。单次修改，需要维护两个版本，一个是$fa$的变化，一个是$rank$的变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include &quot;debug.h&quot;
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#endif

template &amp;lt;typename T&amp;gt;
class PersistentArray {
private:
    struct Node {
        T val;
        int l, r;
        int lson, rson; 

        Node(T val = 0, int l = 0, int r = 0, int lson = -1, int rson = -1)
            : val(val), l(l), r(r), lson(lson), rson(rson) {}
    };

    vector&amp;lt;Node&amp;gt; nodes; 
    vector&amp;lt;int&amp;gt; root;  
    vector&amp;lt;T&amp;gt; data;

    int create(T val, int l, int r, int lson, int rson) {
        nodes.push_back({val, l, r, lson, rson});
        return nodes.size() - 1;
    }

    int build(int l, int r) {
        if (l == r) {
            return create(data[l], l, r, -1, -1);
        } else {
            int mid = (l + r) / 2;
            int left = build(l, mid);
            int right = build(mid + 1, r);
            return create(0, l, r, left, right);
        }
    }

    int revise_path(int u, int pos, const T val) {
        if (nodes[u].l == nodes[u].r) {
            int idx = create(val, nodes[u].l, nodes[u].r, -1, -1);
            return idx;
        } else {
            int lson = nodes[u].lson, rson = nodes[u].rson;
            int mid = (nodes[u].l + nodes[u].r) / 2;
            if (pos &amp;lt;= mid) {
                lson = revise_path(nodes[u].lson, pos, val);
            } else {
                rson = revise_path(nodes[u].rson, pos, val);
            }
            return create(nodes[u].val, nodes[u].l, nodes[u].r, lson, rson);
        }
    }

    T ask_path(int u, int pos) {
        if (nodes[u].l == nodes[u].r)
            return nodes[u].val;
        else {
            int mid = (nodes[u].l + nodes[u].r) / 2;
            if (pos &amp;lt;= mid)
                return ask_path(nodes[u].lson, pos);
            else
                return ask_path(nodes[u].rson, pos);
        }
    }

public:
    PersistentArray(vector&amp;lt;T&amp;gt; initData, int query_cnt = 0) {
        data = move(initData);
        nodes.reserve(data.size() * 4 + query_cnt * 8 * log2(data.size()));
        root.push_back(build(0, data.size() - 1));
    }

    int revise(int v, int pos, const T val) {
        int new_root = revise_path(root[v], pos, val);
        root.push_back(new_root);
        return root.size() - 1;
    }

    T ask(int v, int pos) {
        return ask_path(root[v], pos);
    }

    void copy(int v) {
        root.push_back(root[v]);
    }
};

struct PersistentDSU {
    PersistentArray&amp;lt;int&amp;gt; fa;  // 持久化数组
    PersistentArray&amp;lt;int&amp;gt; rank;

    PersistentDSU(int n, int q = 0): fa(init_array(n), q), rank(vector&amp;lt;int&amp;gt;(n,0), q) { 

    }
    int find(int ver,int x){
        while (x!=fa.ask(ver,x))
            x=fa.ask(ver,x);
        return x;
    }
    void merge(int ver,int x,int y){    // 在版本ver的基础上合并x,y
        x=find(ver,x),y=find(ver,y);
        if (x==y){
            rank.copy(ver);
            fa.copy(ver);
            return ;
        }
        int rx=rank.ask(ver,x);
        int ry=rank.ask(ver,y);
        if (rx==ry)
            rank.revise(ver,y,ry+1);
        else
            rank.copy(ver);
        if (rx&amp;gt;ry)
            fa.revise(ver,y,x);
        else
            fa.revise(ver,x,y);
    }
    void copy(int ver){ // 复制一份版本ver
        rank.copy(ver);
        fa.copy(ver);
    }
private:
    static vector&amp;lt;int&amp;gt; init_array(int n){
        vector&amp;lt;int&amp;gt; data(n);
        iota(data.begin(),data.end(),0);
        return data;
    }
};

int main(){
	int n,q,op,k,a,b;
    cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;q;
    PersistentDSU dsu(n+1, q);
    for (int v=1;v&amp;lt;=q;v++){
        cin&amp;gt;&amp;gt;op;
        if (op==2){
            cin&amp;gt;&amp;gt;k;
            dsu.copy(k);
        }
        else{
            cin&amp;gt;&amp;gt;a&amp;gt;&amp;gt;b;
            if (op==1){
                dsu.merge(v-1,a,b);
            }
            else if (op==3){
            //    debug(a,b);
                a=dsu.find(v-1,a),b=dsu.find(v-1,b);
                cout&amp;lt;&amp;lt;(a==b)&amp;lt;&amp;lt;endl;
                dsu.copy(v-1);
            }
        }

    }
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;可持久化01Trie树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acwing.com/problem/content/258/&quot;&gt;acwing 256 最大异或和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;要求$l\leq p\le r$，使得$(\oplus_{i=p}^n a[i])\oplus x$最大。实际上，对于从高到低每一位，肯定是能1就1，不能1就随便选了，反正是0。所以朴素的想法是，我们需要从高到低，假设总共8位，就从第8位开始，看看选哪些p可以使得为1，然后看第7位，看看我们之前选的那些p中，哪些可以使得第7位为1，一直到最后只有1个p，就是答案。&lt;/p&gt;
&lt;p&gt;这样做就是01Tried树。&lt;/p&gt;
</content:encoded></item><item><title>电赛：HC系列蓝牙模块</title><link>https://fuwari.vercel.app/posts/embed/edc-bt-note/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/edc-bt-note/</guid><pubDate>Sun, 28 Jul 2024 17:43:23 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;调HC蓝牙模块的时候重新记录一下，便于后面查阅。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;HC-05模块（SPP蓝牙）&lt;/h2&gt;
&lt;p&gt;HC05模块是支持主从的SPP蓝牙模块。配置还是比较容易的。&lt;/p&gt;
&lt;p&gt;首先要有两个HC05蓝牙模块和两个USB转TTL，然后按照&lt;code&gt;RX-TX&lt;/code&gt;、&lt;code&gt;TX-RX&lt;/code&gt;成对连接。然后一个计划配置成主机，另一个配置成从机。&lt;/p&gt;
&lt;h3&gt;HC-05主机配置&lt;/h3&gt;
&lt;p&gt;手指长按模块上按键，然后给模块上电，即可进入AT模式。此时模块上的灯慢闪。要是灯没亮就是供电不足，要接5V。&lt;/p&gt;
&lt;p&gt;波特率设成38400，现在发送AT，会回复AT。（注意末尾要加&lt;code&gt;\r\n&lt;/code&gt;，如果串口助手末尾无追加，就要自己输入，不要输出多余的字符）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;① 设置、查询蓝牙名字：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查询名字
AT+NAME?
+NAME:CH-05-Z
OK

# 设置蓝牙名字
AT+NAME=&quot;EDC-master&quot;
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;② 查询设备地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT+ADDR?
+ADDR:0020:12:080515
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 查询设置模块角色&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查询角色
AT+ROLE?
+ROLE:1
OK

# 设置角色（1:主机 0:从机）
AT+ROLE=1
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④ 查询设置配对码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查询
AT+PSWD?
+PSWD:1357
OK

# 设置
AT+PSWD=3332
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④ 正常工作的波特率查询设置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查询
AT+UART?
+UART:9600,0,0
OK

# 设置
AT+UART=115200,0,0
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HC-05从机配置&lt;/h3&gt;
&lt;p&gt;从机同理上电。仿照上面的一顿配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AT
OK

AT+NAME=EDC-slave
OK

AT+ADDR?
+ADDR:98da:50:02904f
OK

AT+ROLE=0
OK

AT+ROLE?
+ROLE:0
OK

AT+PSWD=3332
OK

AT+UART=115200,0,0
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主从连接&lt;/h3&gt;
&lt;p&gt;再次轮到主机进AT模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 绑定从机的地址
AT+BIND=98da:50:02904f
OK

AT+CMODE=0
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检验一下是否连接成功。两个蓝牙上电，这次不按按键了，然后波特率调成刚刚设置的&lt;code&gt;115200&lt;/code&gt;。正常模式下没连接成功是LED闪烁很快，大概等个五秒，LED不再频繁闪烁说明连接成功了。&lt;/p&gt;
</content:encoded></item><item><title>电赛：OpenMV与嵌入式视觉</title><link>https://fuwari.vercel.app/posts/embed/edc-openmv-note/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/edc-openmv-note/</guid><pubDate>Sat, 27 Jul 2024 04:22:08 GMT</pubDate><content:encoded>&lt;p&gt;[TOC]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;两天备战一下电赛。准备 &lt;s&gt;速成&lt;/s&gt; 回顾一下OpenMV芝士，顺便记录一下。&lt;/p&gt;
&lt;p&gt;参考资料：&lt;a href=&quot;https://www.cnblogs.com/brt2/p/13547072.html&quot;&gt;【API】OpenMV常用功能&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;环境配置&lt;/h2&gt;
&lt;p&gt;OpenMV最新版本的IDE感觉有很多问题，因此推荐安装&lt;code&gt;4.0.14&lt;/code&gt;版本的IDE，个人感觉这个版本的是bug比较少、支持固件寄了后直接重新烧录的，非常方便。&lt;/p&gt;
&lt;h2&gt;一. OpenMV基础命令&lt;/h2&gt;
&lt;h3&gt;1. 摄像头基础配置&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://book.openmv.cc/image/sensor.html&quot;&gt;参考文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Cam - By: wlate - Fri Jul 26 2024

import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_auto_gain(False)         # 自动增益
sensor.set_auto_whitebal(False)     # 自动白平衡
sensor.set_auto_exposure(True)      # 自动曝光时间
sensor.skip_frames(time = 2000)

clock = time.clock()

while(True):
    clock.tick()
    img = sensor.snapshot()
    print(clock.fps())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图片是可以访问单个像素点的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img.get_pixel(10,10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会得到一个类似&lt;code&gt;(247, 255, 255)&lt;/code&gt;的值，表示三通道颜色，这里是RGB，也就是说三个通道分别是红色、绿色、蓝色。&lt;/p&gt;
&lt;p&gt;除此之外，如果用的是带畸变的镜头，可以用下面的命令来去畸变：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后效果如下，感觉帧率是没区别的：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkqP3L9&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/26/pkqP3L9.jpg&quot; alt=&quot;pkqP3L9.jpg&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://imgse.com/i/pkqPGZR&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/26/pkqPGZR.jpg&quot; alt=&quot;pkqPGZR.jpg&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2. 统计信息&lt;/h3&gt;
&lt;p&gt;对于一张图片，数字图像处理中，经常需要得到一些统计信息，例如图片某个区域的灰度直方图之类的。这时候要用到ROI，也就是感兴趣区域。&lt;/p&gt;
&lt;p&gt;OpenMV中的ROI，传入四个值$(c,r,w,h)$，也就是左上角开始的列坐标、行坐标、列宽度、行高度。&lt;/p&gt;
&lt;p&gt;下面是绘制了一个感兴趣区域并对区域进行颜色统计的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ROI - By: wlate - Fri Jul 26 2024

import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(10)
sensor.set_auto_whitebal(False)

img = sensor.snapshot()
IMG_COLS = img.width()
IMG_ROWS = img.height()
ROI=(int(IMG_COLS/4),int(IMG_ROWS/4),int(IMG_COLS/2),int(IMG_ROWS/2))

while(True):
    img = sensor.snapshot()
    statistics=img.get_statistics(roi=ROI)
    img.draw_rectangle(ROI)
    
    color_l=statistics.l_mode()
    color_a=statistics.a_mode()
    color_b=statistics.b_mode()
    print(color_l,color_a,color_b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkq9nwq&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/26/pkq9nwq.jpg&quot; alt=&quot;pkq9nwq.jpg&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以看出来框选了图片的重心，串口可以看见打印出来的值。&lt;/p&gt;
&lt;p&gt;这里&lt;code&gt;statistics&lt;/code&gt;能统计的信息如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;统计量&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;索引调用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;平均值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.mean()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度平均数 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[0]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中位数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.median()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度中位数 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;众数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.mode()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度众数 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[2]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;标准差&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.stdev()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度标准差 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[3]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.min()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度最小值 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[4]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.max()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度最大值 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[5]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第一四分数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.lq()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度第一四分数 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[6]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第三四分数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics.uq()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;灰度第三四分数 (0-255)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statistics[7]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;通道&lt;/th&gt;
&lt;th&gt;统计量&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;td&gt;平均值、中位数等&lt;/td&gt;
&lt;td&gt;&lt;code&gt;l_mean&lt;/code&gt;等&lt;/td&gt;
&lt;td&gt;灰度、标准差等统计信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;平均值、中位数等&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a_mean&lt;/code&gt;等&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;平均值、中位数等&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b_mean&lt;/code&gt;等&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里统计的时候用的是LAB格式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L&lt;/strong&gt;：亮度（Luminance），表示颜色的明暗程度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：从绿色到红色的颜色分量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B&lt;/strong&gt;：从蓝色到黄色的颜色分量。&lt;/p&gt;
&lt;p&gt;如果要查看ROI区域中的RGB各个通道值，可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ROI - By: wlate - Fri Jul 26 2024

import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(10)
sensor.set_auto_whitebal(False)

img = sensor.snapshot()
IMG_COLS = img.width()
IMG_ROWS = img.height()
ROI=(int(IMG_COLS/4),int(IMG_ROWS/4),int(IMG_COLS/2),int(IMG_ROWS/2))

while(True):
    img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0)   # 去畸变
    img = img.to_grayscale(copy=True, rgb_channel=0)    # 筛出红色通道
    
    statistics=img.get_statistics(roi=ROI)
    img.draw_rectangle(ROI)

    print(&quot;Red Channel Mean:&quot;, statistics.mean())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里&lt;code&gt;img.to_grayscale(copy=True, rgb_channel=0)&lt;/code&gt;意思是只对红色通道做转换，这里需要一点数字图像处理的基本芝士。一般来说灰度图，每个像素的灰度是0~255，0是最暗，也就是黑色，255是最亮，也就是白色。3色8位图，每个通道也是0~255。R通道，也就是红色通道，0是几乎没有，255是最红，其他同理。这里我们只筛选出红色通道，也就是说对绿色和蓝色滤波了。&lt;/p&gt;
&lt;h3&gt;3. 调试方法&lt;/h3&gt;
&lt;p&gt;在跑OpenMV的时候，如果一直跑循环串口流肯定不太方便，有的时候需要对单帧图片分析，可以对于我们需要测试的图片，单独读入后分析。&lt;/p&gt;
&lt;p&gt;首先把图片保存在OpenMV的磁盘上，假设是&lt;code&gt;1.jpg&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;二. 传统cv算法识别&lt;/h2&gt;
&lt;h3&gt;1. 识别红绿激光&lt;/h3&gt;
&lt;p&gt;红绿激光应该是电赛典中典的识别项目了，可以直接调用cv函数来识别。思路主要是红绿激光点的特征。这很容易想到：① 是圆形 ② 颜色。&lt;/p&gt;
&lt;p&gt;首先可以先实现一个针对单色激光点的，也就是在滤波后找最圆的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(time = 2000)
sensor.set_gainceiling(8)           # 适用颜色识别的增益
sensor.set_auto_whitebal(False)
clock = time.clock()

circle_thres = 500

while(True):
    img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0)  # 去畸变
    gray_red = img.to_grayscale(copy=False, rgb_channel=0)

    gray_red = gray_red.mean(1)

    circles_red = gray_red.find_circles(threshold = circle_thres,  # 阈值，用于控制圆形检测的灵敏度
                                       x_margin = 10,      # X轴边距
                                       y_margin = 10,      # Y轴边距
                                       r_margin = 10,      # 半径边距
                                       r_min = 1,          # 最小半径
                                       r_max = 10,         # 最大半径
                                       r_step = 1)         # 半径步长

    if circles_red:
        for c in circles_red:
            img.draw_cross(c.x(), c.y(), color = (255, 0, 0)) 
            print(&quot;Circle: x = %d, y = %d, r = %d, strength = %.2f&quot; % (c.x(), c.y(), c.r(), c.magnitude()))  # 打印圆心坐标、半径和置信度
            print(&quot;Detected circle - Radius: %d&quot; % c.r())

    else:
        print(&quot;None&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;背景会造成误差，因此在摄像头不动的情况下，例如23年电赛E题，可以通过开机判断外矩形后，划定ROI，就不会错误识别到背景了。&lt;/p&gt;
&lt;p&gt;接下来是区别红绿激光，我的思路是，因为红绿激光亮度都很高，所以先在画面中找符合要求的圆，然后找两个亮度最高，其他都扔掉。然后对于这两个，对比A通道，设定阈值即可。应该能一定程度避免噪声。（此外，硬件上可以通过调焦更好识别激光点，焦距模糊后能识别的更好）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(time = 2000)
sensor.set_gainceiling(8)           # 适用颜色识别的增益
sensor.set_auto_whitebal(False)
clock = time.clock()

circle_thres = 500
offs = 2
color_thres = 0

while(True):
    img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0)  # 去畸变
    gray = img.to_grayscale(copy=True)
    gray = gray.mean(1)

    circles_red = gray.find_circles(threshold = circle_thres,  # 阈值，用于控制圆形检测的灵敏度
                                       x_margin = 10,          # X轴边距
                                       y_margin = 10,          # Y轴边距
                                       r_margin = 10,          # 半径边距
                                       r_min = 1,              # 最小半径
                                       r_max = 10,             # 最大半径
                                       r_step = 1)             # 半径步长

    if circles_red:
        circle_data = []
        for i, c in enumerate(circles_red):
            roi = (c.x() - c.r() - offs , c.y() - c.r() -offs , 2 * ( c.r() + offs ), 2 * ( c.r() + offs ))
            statis = img.get_statistics(roi=roi)
            brightness = statis.l_mode()
            color = statis.a_mode()
            circle_data.append((c, brightness, color))
        circle_data.sort(key=lambda x: (-x[1]))

        if len(circle_data) &amp;gt;= 2:
            circle_data = circle_data[:2]
            color_thres = ( circle_data[0][2] + circle_data[1][2] ) / 2

        for idx, (circ, _, color) in enumerate(circle_data[:2]):
            img.draw_rectangle(circ.x() - circ.r(), circ.y() - circ.r(), 2 * circ.r(), 2 * circ.r(), color=(255, 255, 0))

            if color &amp;gt; color_thres:
                img.draw_string(circ.x() - circ.r(), circ.y() - circ.r() - 10, &quot;RED&quot;, color=(255, 0, 0))
            #    print(&quot;RED&quot;, color)
            else:
                img.draw_string(circ.x() - circ.r(), circ.y() - circ.r() - 10, &quot;GREEN&quot;, color=(0, 255, 0))
            #    print(&quot;Green&quot;, color)

        print(&quot;Thres&quot;, color_thres)

    else:
        print(&quot;None&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. ROI跟踪&lt;/h3&gt;
&lt;p&gt;有空再写这个。&lt;/p&gt;
&lt;h2&gt;三. TensorFlow Lite神经网络识别&lt;/h2&gt;
&lt;p&gt;虽然能用传统算法一般传统算法更快更准，但是有的时候会有传统算法很难办到的识别任务，还得用神经网络。&lt;/p&gt;
&lt;p&gt;在OpenMV上跑的神经网络，一般都是在&lt;a href=&quot;https://studio.edgeimpulse.com/studio/profile/projects?autoredirect=1&quot;&gt;Edge Impulse&lt;/a&gt;上训练的。因为正好在做一个物联网项目，这里就记录一下利用Edge Impulse训练一个手势识别项目&lt;code&gt;Gesture Recognition&lt;/code&gt;为例子，这是个目标检测模型。&lt;/p&gt;
&lt;h3&gt;1. 数据采集&lt;/h3&gt;
&lt;p&gt;OpenMV的IDE应该是可以实现数据集采集，但是看上去很麻烦，并且我的版本IDE采集会乱码，因此我选择更简便的方式，通过IDE的流来录制手势视频，然后用python十几帧采集一个作为结果。&lt;/p&gt;
&lt;p&gt;注意，不要缩放视频。&lt;/p&gt;
&lt;p&gt;利用python写的10帧切割视频代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;&apos;&apos;
Author: wlaten
Date: 2024-07-27 02:52:06
LastEditTime: 2024-07-27 03:03:42
Discription: file content
&apos;&apos;&apos;
import cv2
import os
import glob

def export_frames(video_path, k, label):
    &quot;&quot;&quot;
    导出视频的帧（每k帧）到output/label文件夹下    
    &quot;&quot;&quot;
    output_folder = os.path.join(&apos;output&apos;, label)
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
    existing_files = glob.glob(os.path.join(output_folder, &apos;*.jpg&apos;))
    if existing_files:
        last_num = max([int(os.path.basename(f).split(&apos;.&apos;)[0]) for f in existing_files])
    else:
        last_num = 0
    cap = cv2.VideoCapture(video_path)
    frame_count = 0
    
    cnt = 0
    l = last_num + 1
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        if frame_count % k == 0:
            last_num += 1
            output_path = os.path.join(output_folder, f&apos;{last_num}.jpg&apos;)
            cv2.imwrite(output_path, frame)
            cnt += 1

        frame_count += 1
    cap.release()
    print(f&apos;导出{cnt}帧到{output_folder}&apos;)
    if cnt &amp;gt; 0:
        print(f&apos;编号: {l} -&amp;gt; {last_num}&apos;)

def delete_frames(start, end, label):
    &quot;&quot;&quot;
    删除output/label文件夹下编号为start到end的帧（防止不小心放错标签集）
    &quot;&quot;&quot;
    output_folder = os.path.join(&apos;output&apos;, label)
    for i in range(start, end+1):
        output_path = os.path.join(output_folder, f&apos;{i}.jpg&apos;)
        if os.path.exists(output_path):
            os.remove(output_path)
            print(f&apos;删除{output_path}&apos;)
        else:
            print(f&apos;{output_path}不存在&apos;)

if __name__ == &apos;__main__&apos;:
    export_frames(&apos;4.mp4&apos;, 10, &apos;scissor&apos;)
    delete_frames(197,332, &apos;scissor&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 模型训练&lt;/h3&gt;
&lt;p&gt;在Edge Impluse上，新建一个项目，然后&lt;code&gt;Project Info&lt;/code&gt;里面，&lt;code&gt;Labeling method&lt;/code&gt;选&lt;code&gt;Bounding Boxes&lt;/code&gt;，&lt;code&gt;Target Device&lt;/code&gt;选&lt;code&gt;OpenMV Cam H7 Plus&lt;/code&gt;。然后就是普通的采集和标注了。最后直接导出OpenMV适用的&lt;code&gt;tflite&lt;/code&gt;模型即可。&lt;/p&gt;
&lt;p&gt;参考这个：&lt;a href=&quot;https://blog.csdn.net/qq_36300069/article/details/118071444&quot;&gt;OpenMV 从入手到跑TensorFlow Lite神经网络进行垃圾分类&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不过Edge Impulse太慢了，有机会研究一下手写训练过程，不想用这个平台了。&lt;/p&gt;
&lt;p&gt;然后就是训练，我使用了170张剪刀手的数据集。最后平台验证集正确率在98%。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkqlALq&quot;&gt;&amp;lt;img src=&quot;https://s21.ax1x.com/2024/07/27/pkqlALq.png&quot; alt=&quot;pkqlALq.png&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是这里是理论正确率，做过嵌入式神经网络的都懂，这种理论正确率不代表实际部署后也能这么高，实际环境下只可能更坏（）。&lt;/p&gt;
&lt;h3&gt;3. 模型部署&lt;/h3&gt;
&lt;p&gt;将模型py文件、代码&lt;code&gt;trained.tflite&lt;/code&gt;、标签&lt;code&gt;labels.txt&lt;/code&gt;都放入OpenMV的盘中后即可。效果如图：
&lt;a href=&quot;https://imgse.com/i/pkqlpFS&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/27/pkqlpFS.jpg&quot; alt=&quot;pkqlpFS.jpg&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;图像格式这样设置的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样设置图片很小，只有&lt;code&gt;60*80&lt;/code&gt;，但是不影响检测，小图片跑神经网络帧率还可以的，实测OpenMV Cam H7 Plus能跑到40帧。&lt;/p&gt;
&lt;p&gt;对手势位置绘制十字的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Edge Impulse - OpenMV Object Detection Example

import sensor, image, time, os, tf, math, uos, gc

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA)
sensor.set_auto_gain(True)
sensor.set_auto_whitebal(True)
sensor.skip_frames(time=2000)

net = None
labels = None
min_confidence = 0.98

try:
    # load the model, alloc the model file on the heap if we have at least 64K free after loading
    net = tf.load(&quot;trained.tflite&quot;, load_to_fb=uos.stat(&apos;trained.tflite&apos;)[6] &amp;gt; (gc.mem_free() - (64*1024)))
except Exception as e:
    raise Exception(&apos;Failed to load &quot;trained.tflite&quot;, did you copy the .tflite and labels.txt file onto the mass-storage device? (&apos; + str(e) + &apos;)&apos;)

try:
    labels = [line.rstrip(&apos;\n&apos;) for line in open(&quot;labels.txt&quot;)]
except Exception as e:
    raise Exception(&apos;Failed to load &quot;labels.txt&quot;, did you copy the .tflite and labels.txt file onto the mass-storage device? (&apos; + str(e) + &apos;)&apos;)

colors = [ # Add more colors if you are detecting more than 7 types of classes at once.
    (255,   0,   0),
    (  0, 255,   0),
    (255, 255,   0),
    (  0,   0, 255),
    (255,   0, 255),
    (  0, 255, 255),
    (255, 255, 255),
]

while(True):
    img = sensor.snapshot()

    for i, detection_list in enumerate(net.detect(img, thresholds=[(math.ceil(min_confidence * 255), 255)])):
        if (i == 0): continue  # 忽略背景类
        if (len(detection_list) == 0): continue  # 当前类别无检测对象

        print(&quot;********** %s **********&quot; % labels[i])
        for d in detection_list:
            [x, y, w, h] = d.rect()  # 获取边界框坐标和大小
            confidence = d.output()  # 直接获取置信度
            center_x = math.floor(x + (w / 2))
            center_y = math.floor(y + (h / 2))

            # 打印坐标和置信度
            print(&apos;x %d\ty %d\tw %d\th %d\tconfidence %.2f&apos; % (x, y, w, h, confidence))

            # 在边界框中心绘制十字标记
            img.draw_cross(center_x, center_y, color=colors[i], size=10, thickness=2)  # size 控制十字的大小
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Codeforces Round 958 (Div. 2) solutions (A-D)</title><link>https://fuwari.vercel.app/posts/acm/codeforces-round-958-div-2-solutions-a-d/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/codeforces-round-958-div-2-solutions-a-d/</guid><pubDate>Tue, 16 Jul 2024 02:52:16 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1988/problem/A&quot;&gt;A - Split the Multiset&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;正着做是从&lt;code&gt;n&lt;/code&gt;拆解到&lt;code&gt;1&lt;/code&gt;，反着就是从&lt;code&gt;n&lt;/code&gt;个&lt;code&gt;1&lt;/code&gt;合并到&lt;code&gt;n&lt;/code&gt;，那就尽可能每次合并&lt;code&gt;k&lt;/code&gt;个，模拟一下就行（虽然感觉正着想反着想没区别）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void solve(void){
    int n,k;
    cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    int ans=0;
    while (n&amp;gt;=k){
        ans+=n/k;
        n=(n/k)+(n%k);
    }
    cout&amp;lt;&amp;lt;ans+(n!=1)&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1988/problem/B&quot;&gt;B - Make Majority&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;首先全1肯定直接YES了，全0也直接NO。然后从样例数据可以看出来，直接把连续的0合并成一个0，最优。然后是从有0有1的段落进一步合并，因为每次我们要把&lt;code&gt;k&lt;/code&gt;个0改掉，要付出至少&lt;code&gt;k+1&lt;/code&gt;个&lt;code&gt;1&lt;/code&gt;，这样1的个数是随着吞并次数的增加越来越少的，因此我们直接进行一次&lt;code&gt;[1,n]&lt;/code&gt;的吞并，显然如果这次不能把所有数都吞成1，之后也再也不会有机会的。直接判断即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;
char str[maxn+5];
 
void solve(void){
    scanf(&quot;%d&quot;,&amp;amp;n);
    scanf(&quot;%s&quot;,str+1);
    char ch=str[1];
    bool allsame=true;
    for (int i=2;i&amp;lt;=n&amp;amp;&amp;amp;allsame;i++)
        if (str[i]!=ch)
            allsame=false;
    if (allsame){
        ch==&apos;1&apos;?puts(&quot;Yes&quot;):puts(&quot;No&quot;);
        return ;
    }
    vector&amp;lt;P&amp;gt; block;
    for (int i=1;i&amp;lt;=n;i++)
        if (str[i]!=str[i-1])
            block.push_back(P{str[i],1});
        else
            block.back().second++;
    int cnt_zero=0,cnt_one=0;
    for (P p:block)
        if (p.first==&apos;0&apos;)
            p.second=1,cnt_zero++;
        else
            cnt_one+=p.second;
    if (cnt_one&amp;gt;cnt_zero){
        puts(&quot;Yes&quot;);
        return ;
    }
    else
        puts(&quot;No&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1988/problem/C&quot;&gt;C - Increasing Sequence with Fixed OR&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;一开始的时候想到&lt;code&gt;i=(i-1)&amp;amp;mask&lt;/code&gt;去了，浪费了一点时间（漏条件了，草）。观察样例可以发现最后一个数必定是&lt;code&gt;n&lt;/code&gt;，那就从后往前推，倒数第二个要比&lt;code&gt;n&lt;/code&gt;小一点，但是和&lt;code&gt;n&lt;/code&gt;或运算后要等于&lt;code&gt;n&lt;/code&gt;，那就只能从&lt;code&gt;n&lt;/code&gt;的二进制位里面删掉一个&lt;code&gt;1&lt;/code&gt;来构造了，肯定是删最小的更优。倒数第三个因为倒数第二个已经失去了&lt;code&gt;n&lt;/code&gt;的二进制位里的一个&lt;code&gt;1&lt;/code&gt;，要求倒二倒三或运算后等于&lt;code&gt;n&lt;/code&gt;，这个&lt;code&gt;1&lt;/code&gt;要由倒数第三个来补，补上了低位的&lt;code&gt;1&lt;/code&gt;后还要保证比倒数第二个小，那就要扣掉前面的&lt;code&gt;1&lt;/code&gt;，因此构造就很明显了，假设
$$
n=(1111011110)_2\
$$
那就会有
$$
ans_k=(1111011110)&lt;em&gt;2\
ans&lt;/em&gt;{k-1}=(1111011100)&lt;em&gt;2\
ans&lt;/em&gt;{k-2}=(1111011010)&lt;em&gt;2\
ans&lt;/em&gt;{k-3}=(1111010110)&lt;em&gt;2\
ans&lt;/em&gt;{k-4}=(1111001110)&lt;em&gt;2\
ans&lt;/em&gt;{k-5}=(1110011110)_2\
\vdots\
ans_1=(0111011110)_2
$$
到这里就结束了，可以证明这就是最长的$k$。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef long long ll;

ll n;
#define lowbit(x) ((x)&amp;amp;(-(x)))
 
void solve(void){
    cin&amp;gt;&amp;gt;n;
    vector&amp;lt;ll&amp;gt; ans;
    ll mask=n;
    ll x=n;
    ans.push_back(n);
    while (x){
        if (n-lowbit(x))
            ans.push_back(n-lowbit(x));
        x^=lowbit(x);
    }
    cout&amp;lt;&amp;lt;ans.size()&amp;lt;&amp;lt;endl;
    for (int i=ans.size()-1;i&amp;gt;=0;i--)
        cout&amp;lt;&amp;lt;ans[i]&amp;lt;&amp;lt;&apos; &apos;;
    cout&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1988/problem/D&quot;&gt;D - The Omnipotent Monster Killer&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;感觉样例在暗示贪心，但是稍微想一下就感觉贪心不可能。然后往dp想就被硬控一个小时，最后写完发现时间已经结束了TT（还是太彩了）&lt;/p&gt;
&lt;p&gt;一个能否掉贪心的样例是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：
1
7
1 1 10000 10000 1 1 1
1 2
1 3
2 4
2 5
4 6
4 7

输出：
20010
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设怪物&lt;code&gt;u&lt;/code&gt;在&lt;code&gt;k&lt;/code&gt;回合被杀掉，它给全局生命减量提供的贡献是&lt;code&gt;a[u]*k&lt;/code&gt;。对于相邻顶点，他们最终贡献给全局答案的&lt;code&gt;k&lt;/code&gt;回合是不同的。&lt;/p&gt;
&lt;p&gt;可以猜一下这个最终回合数不会太大，因为太大提供不了更优，并且如果黑白染色后，这个结果已经是比较优的，也就是说我们每次多一个回合，至少能让存活的&lt;code&gt;a&lt;/code&gt;值和减少一半。一个合理的猜测是这个最终回合数应该是$\log n$级别的（实际上是$k\geq \lfloor \log_2n\rfloor +1$）。&lt;/p&gt;
&lt;p&gt;那直接dp了。设&lt;code&gt;dp[u][k]&lt;/code&gt;表示假设在&lt;code&gt;k&lt;/code&gt;轮击杀&lt;code&gt;u&lt;/code&gt;怪物，&lt;code&gt;u&lt;/code&gt;的子树内的伤害总和最小值。在不考虑条件的情况下，dp递推是这样的
$$
dp[u][k]=k\times a[u]+\sum \min_{t\in[1,\max]} (dp[v][t])
$$
但是必须考虑条件。假设&lt;code&gt;v&lt;/code&gt;是&lt;code&gt;u&lt;/code&gt;的任意一个儿子，因为&lt;code&gt;u&lt;/code&gt;不可以和&lt;code&gt;v&lt;/code&gt;在同一个轮次被击杀，所以是
$$
dp[u][k]=k\times a[u]+\sum \min_{t\in[1,\max] ,t\neq k} (dp[v][t])
$$
因此一个朴素的写法是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;
ll a[maxn+5];

vector&amp;lt;int&amp;gt; G[maxn+5];

int max_cnt;
ll dp[maxn+5][25]; 

void dfs(int u,int fa){
    for (int k=1;k&amp;lt;=max_cnt;k++)
        dp[u][k]=k*a[u];
    for (int v: G[u])
        if (v!=fa){
            dfs(v,u);
            for (int k=1;k&amp;lt;=max_cnt;k++){
                ll delta=inf;
                for (int j=1;j&amp;lt;=max_cnt;j++)
                    if (k!=j)
                        delta=min(delta,dp[v][j]);
                dp[u][k]+=delta;
            }
        }
}

void solve(void){
    cin&amp;gt;&amp;gt;n;
    max_cnt=(int)log2(n)+1;
    memset(dp,127,sizeof(dp));
    for (int i=1;i&amp;lt;=n;i++)
        G[i].clear();
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1,0);
    ll ans=inf;
    for (int k=1;k&amp;lt;=max_cnt;k++)
        ans=min(ans,dp[1][k]);
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个写法会TLE。仔细分析一下复杂度应该是$O(n\log^2n)$，可能常数小大T了。仔细看看可以优化这段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		for (int k=1;k&amp;lt;=max_cnt;k++){
                ll delta=inf;
                for (int j=1;j&amp;lt;=max_cnt;j++)
                    if (k!=j)
                        delta=min(delta,dp[v][j]);
                dp[u][k]+=delta;
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的$\min dp[v][j]$，实际上可以直接贡献给&lt;code&gt;max_cnt-1&lt;/code&gt;的&lt;code&gt;dp[u][k]&lt;/code&gt;，剩下一个和轮次重复的，我们取次小值即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;
ll a[maxn+5];

vector&amp;lt;int&amp;gt; G[maxn+5];

int max_cnt;
ll dp[maxn+5][25]; 

void dfs(int u,int fa){
    for (int k=1;k&amp;lt;=max_cnt;k++)
        dp[u][k]=k*a[u];
    for (int v: G[u])
        if (v!=fa){
            dfs(v,u);
            ll minn=inf,min2=inf;
            int pos;
            for (int k=1;k&amp;lt;=max_cnt;k++)
                if (dp[v][k]&amp;lt;minn)
                    min2=minn,minn=dp[v][k],pos=k;
                else if (dp[v][k]&amp;lt;min2)
                    min2=dp[v][k];
            for (int k=1;k&amp;lt;=max_cnt;k++)
                dp[u][k]+=(k!=pos)?minn:min2;
        }
}

void solve(void){
    cin&amp;gt;&amp;gt;n;
    max_cnt=(int)log2(n)+1;
    for (int i=1;i&amp;lt;=n;i++)
        for (int k=1;k&amp;lt;=max_cnt;k++)
            dp[i][k]=inf;
    for (int i=1;i&amp;lt;=n;i++)
        G[i].clear();
    for (int i=1;i&amp;lt;=n;i++)
        cin&amp;gt;&amp;gt;a[i];
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1,0);
    ll ans=inf;
    for (int k=1;k&amp;lt;=max_cnt;k++)
        ans=min(ans,dp[1][k]);
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Codeforces Round 926 (Div. 2) solutions (A-D)</title><link>https://fuwari.vercel.app/posts/acm/codeforces-round-926-div-2-solutions-a-d/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/acm/codeforces-round-926-div-2-solutions-a-d/</guid><pubDate>Mon, 15 Jul 2024 01:48:53 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://codeforces.com/contest/1929&quot;&gt;Codeforces Round 926 (Div. 2)&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1929/problem/A&quot;&gt;A - Sasha and the Beautiful Array&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;直接找最大和最小的数取差值即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;
 
void solve(void){
    cin&amp;gt;&amp;gt;n;
    int minn=INT_MAX,maxx=0;
    for (int i=1,x;i&amp;lt;=n;i++){
        cin&amp;gt;&amp;gt;x;
        minn=min(minn,x);
        maxx=max(maxx,x);
    }
    cout&amp;lt;&amp;lt;maxx-minn&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1929/problem/B&quot;&gt;B - Sasha and the Drawing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;分类讨论一下，很显然，我们直接第一行逐个取格子，第一行是每个格子都可以提供$2$的新对角线贡献的。$k$小等于于$2n$的时候，直接输出$\lceil k/2\rceil$即可。如果$k\geq 2n$，也就是说第一行的格子提供的贡献不够了，再来看最后一行，最后一行除了第一个和第二个格子，每个也可以提供$2n$的贡献，因此在$k\leq 2(n+n-2)$的情况下，我们也可以同理输出$\lfloor k/2\rfloor$。然后剩下就只有两种情况，$4n-4&amp;lt;n\leq 4n-2$，这两种情况，我们把最后一行剩下两个格子取了就行。&lt;/p&gt;
&lt;p&gt;（这里我第一次wa了，后面乱改改过了，发现是因为&lt;code&gt;cout&lt;/code&gt;输出&lt;code&gt;double&lt;/code&gt;类型会出现科学计数法的问题，这里&lt;code&gt;ceil&lt;/code&gt;本来是&lt;code&gt;double&lt;/code&gt;类型的，要强制转成&lt;code&gt;long long&lt;/code&gt;或者其他整型。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void solve(void){
    ll n,k;
    cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;k;
    if (k&amp;lt;=2*n){
        cout&amp;lt;&amp;lt;(ll)(ceil((double)k/2))&amp;lt;&amp;lt;endl;
        return ;
    }
    ll ans=n;
    k-=2*n;
    if (k&amp;lt;=2*(n-2)){
        cout&amp;lt;&amp;lt;(ll)(ans+ceil((double)k/2))&amp;lt;&amp;lt;endl;
        return ;
    }
    k-=2*(n-2);
    ans+=n-2;
    cout&amp;lt;&amp;lt;(ll)(ans+k)&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1929/problem/C&quot;&gt;C - Sasha and the Casino&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这题有点像那个经典的赌场问题，就是在本金无限大的情况下，不断赌博是必赢的，因为只要每一次下的赌注足够大，大到赢了一次的奖励能大于前面的损失，即使不断输，只要赢一次，就能翻本。比如假设输赢的概率五五开，第一次赌1块，输了，那第二次赌2块，赢了就能赢4块，翻本。如果还输，就赌8块，以此类推。&lt;/p&gt;
&lt;p&gt;所以只需要$1$到$x$次，假设一直输，下的赌注每次都使得如果赢的奖金能翻本并且多一点即可。如果没赌到$x$次，本金就不够了，那就必输了，前面的$1$到$x$次，因为我们下的赌注是赢一次能翻本的，所以只要能赢一次，就能继续从头开始继续赢。但是这里我们要求最坏情况下，如果赌到$x$次都一直输，接下来就是必胜局了，直接赌上所有剩余本金，如果这次赢的能覆盖之前的所有损失，就是必赢的，如果不能，就是必输的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ll k,x,a;
 
void solve(void){
    cin&amp;gt;&amp;gt;k&amp;gt;&amp;gt;x&amp;gt;&amp;gt;a;
    ll now=a;
    debug(a);
    for (int i=0;i&amp;lt;x;i++){
        ll y=max(1LL,(ll)floor((double)(a-now)/(double)(k-1))+1);
        now-=y;
        debug(i,y,now);
        if (now&amp;lt;=0){
            cout&amp;lt;&amp;lt;&quot;NO&quot;&amp;lt;&amp;lt;endl;
            return ;
        }
    }
    now*=k;
    if (now&amp;gt;a)
        cout&amp;lt;&amp;lt;&quot;YES&quot;&amp;lt;&amp;lt;endl;
    else
        cout&amp;lt;&amp;lt;&quot;NO&quot;&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/contest/1929/problem/D&quot;&gt;D - Sasha and a Walk in the City&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;很显然最终集合的最大的规模，是这棵树的叶子节点数。&lt;/p&gt;
&lt;p&gt;既然是树上问题，考虑一下dp，每个节点&lt;code&gt;u&lt;/code&gt;的子树中，设&lt;code&gt;dp[u]&lt;/code&gt;为节点&lt;code&gt;u&lt;/code&gt;及其子树中的满足不存在任何直接祖先关系的集合的大小，同时要注意，单个节点占据路口也是可以的。假设&lt;code&gt;u&lt;/code&gt;节点有一堆儿子&lt;code&gt;v1,v2,...,vm&lt;/code&gt;，可以得到递推方程：
$$
dp[u]=\prod(1+dp[v])
$$
那直接取模就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;
vector&amp;lt;int&amp;gt; G[maxn+5];

ll dp[maxn+5];

void dfs(int u,int fa){
    for (int v: G[u])
        if (v!=fa){
            dfs(v,u);
            dp[u]*=(dp[v]+1);
            dp[u]%=mod;
        }
}

void prepare(){
    for (int i=0;i&amp;lt;=n;i++){
        dp[i]=1;
        G[i].clear();
    }
}

void solve(void){
    cin&amp;gt;&amp;gt;n;
    prepare();
    vector&amp;lt;int&amp;gt; in(n+1,0);
    for (int i=1,u,v;i&amp;lt;n;i++){
        cin&amp;gt;&amp;gt;u&amp;gt;&amp;gt;v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1,0);
    ll ans=0;
    for (int i=0;i&amp;lt;=n;i++)
        ans+=dp[i],ans%=mod;
    cout&amp;lt;&amp;lt;ans&amp;lt;&amp;lt;endl;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>智能车二值化和边缘提取</title><link>https://fuwari.vercel.app/posts/embed/smart-car-binarization-and-edge-extraction/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/smart-car-binarization-and-edge-extraction/</guid><pubDate>Thu, 15 Feb 2024 22:14:54 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;搬运一下之前写的博客。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;寒假浅浅研究了一下智能车赛中的极端光照和赛道反光的解决方案，记录一点思考。&lt;/p&gt;
&lt;p&gt;一般来讲，现在比赛中比较流行的针对不同光照强度的算法是otsu算法（大津法）。这种算法针对不同的光照条件动态设置全局阈值，解决了在宏观上光照条件不同的情况，并且经过前缀和优化和间接采样的otsu，时间上非常快，TC264芯片一般2ms以内可以跑完一张图（好像是）。&lt;/p&gt;
&lt;p&gt;但是这个方法但是无法很好解决小范围光照不同的情况。例如在一张图中，前半部分和后半部分光照不同，或者更极端一点，反光的情况，如下图。&lt;/p&gt;
&lt;p&gt;原图（反光情况）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIF9hQ&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIF9hQ.jpg&quot; alt=&quot;pkIF9hQ.jpg&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;OTSU全局处理结果&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIFSAS&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIFSAS.png&quot; alt=&quot;pkIFSAS.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这种情况下，只用otsu就不太够用。虽然可以通过硬件上增加偏振片来一定程度处理，但是感觉也要在算法上为可能的逆天光照做一点准备。&lt;/p&gt;
&lt;h2&gt;直方图均衡化&lt;/h2&gt;
&lt;p&gt;参考文章：https://blog.csdn.net/weixin_43981425/article/details/106884136&lt;/p&gt;
&lt;p&gt;这篇文章里写可以考虑将灰度值重新映射，重新分配灰度值，使得直方图均衡。感觉这个思路很有意思，而且实现起来速度会很快。&lt;/p&gt;
&lt;p&gt;但是仔细想想我认为不太可行。因为实际上我们要解决的问题的全图光照不均匀，也就意味着部分区域的灰度值和另一些区域的灰度值，虽然一样，但是在不同的区域代表的意义不一样，全图重新分配，实际上并不能解决这个问题。&lt;/p&gt;
&lt;h2&gt;边缘检测算法：Canny算法等&lt;/h2&gt;
&lt;p&gt;前几天浅浅研究了一下这些边缘检测算法，感觉是可行的。虽然说全图做卷积这个太为难单片机算力了，但是我认为可以对图片做模糊化，缩小图片大小，对模糊化后结果再跑Canny之类的边缘检测算法，感觉应该是可以承受这个计算的。&lt;/p&gt;
&lt;p&gt;网上的思路比较多用sobel算子，但是我针对反光的情况跑了一下，感觉sobel算子不如拉普拉斯算子和Roberts算子。下图是二者的处理结果，是2倍缩略图下处理后再做非极大值抑制的结果。&lt;/p&gt;
&lt;p&gt;拉普拉斯算子&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIix78&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIix78.png&quot; alt=&quot;pkIix78.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Roberts算子&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIFptg&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIFptg.png&quot; alt=&quot;pkIFptg.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;处理了一下感觉仍然不太可行，针对反光处可以看出Roberts算子的边缘已经断开了，拉普拉斯算子的边缘虽然没有断开，已经不太明显了。感觉此类的边缘检测在部分区域极端反光下，比起全局阈值的otsu稍微优秀一点，但是没有显著优势（毕竟做卷积需要花算力，增加处理的时间，并且极端反光效果也只是好一点）。&lt;/p&gt;
&lt;h2&gt;波函数均匀化&lt;/h2&gt;
&lt;p&gt;读了一些论文，有一些论文在做OCR的时候，针对全局去算对比度或者亮度的函数，然后做一个变换使得均匀化，我感觉这个思路是可以的，但是智能车赛道上的情况不一样，比起文本识别的时候书页的函数，波动更大（例如，拍到墙壁或者非赛道背景的情况）。如果采用这个思路的话，感觉可以做一个逆透视后只针对地面情况做变换，但是我还没来得及去试一试怎么实现。&lt;/p&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;在 上海交通大学AuTop战队开源算法讲解(一)二值化的技巧 中提到了类似的局部阈值法，原话是这样的：&lt;/p&gt;
&lt;p&gt;对于每个像素点，取它周围一小片区域计算得出一个二值化阈值，并对这个像素进行二值化。参考之前的讲过的大津法，一小片区域的二值化阈值可以通过中位数，平均数或者大津法确定&lt;/p&gt;
&lt;h2&gt;均值阈值法&lt;/h2&gt;
&lt;p&gt;在上交开源算法中，采取的是平均数做阈值，这个实现起来不难，但是复杂度其实比较高。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 上交开源的代码片段
for(int dy=-half_block; dy&amp;lt;=half_block; dy++){
    for(int dx=-half_block; dx&amp;lt;=half_block; dx++){
        thres += img_data[(x+dx)+(y+dy)*width];
    }
}
thres = thres / (block * block) - clip_value;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设这个区域大小是$w&lt;em&gt;w$的（实际操作中这个值不能太小，太小就没意义了），如果对于每个像素点，都像这种做法一样朴素地用二维遍历计算阈值，假设我们边缘跟踪的像素线条长度是$L$，那做一次搜线复杂度就是$O(w&lt;/em&gt;w*L)$。&lt;/p&gt;
&lt;p&gt;但是实际上我们可以用滑动窗口优化。因为窗口大小固定，所以在边缘跟踪的时候只需要维护窗口内灰度值的和。如果我们已知上一个像素点的窗口和，假设我们下一个像素点是上移的，那么只需要减去旧窗口下侧边，再加上新窗口上侧边，即可得到新窗口的灰度值和。这样做将二维遍历压缩成一维遍历，做一次搜线复杂度是$O(w*L)$。&lt;/p&gt;
&lt;p&gt;（在校赛的时候我就是这样写的，效果还可以。）&lt;/p&gt;
&lt;h2&gt;Bernsen算法&lt;/h2&gt;
&lt;p&gt;或者我们可以不采用平均数，而采用Bernsen算法。Bernsen算法和前面灰度补偿均匀化对比度的思路有点像，针对的是对比度。&lt;/p&gt;
&lt;p&gt;参考文章：&lt;a href=&quot;https://wenku.baidu.com/view/9e00c759b84ae45c3b358c48.html?_wkts_=1707712180980&amp;amp;bdQuery=Berson%E7%AE%97%E6%B3%95&quot;&gt;bernsen算法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;朴素的Bernsen中，设以当前像素为中心的w*w窗口内，所有像素的最大值是M，最小值是N，两者的均值T。&lt;/p&gt;
&lt;p&gt;如果 $(M-N)&amp;gt;S$（$S$是设置的一个对比度阈值），则当前点P的阈值为$T$。否则说明当前窗口所在区域的灰度级差别较小，那么窗口在目标区或在背景区，若$T&amp;gt;T&apos;$，则当前点灰度值为$255$，否则，当前点灰度值为$0$。S作者最初设为$15$，$T&apos;$设为$(255+0)/2=128$。也可以用其他方法改进，大多数论文采用高斯滤波进行改进。不过因为智能车赛道比较简单，噪声其实也不多，所以我感觉高斯滤波没有什么必要。&lt;/p&gt;
&lt;p&gt;在边缘跟踪的时候，动态维护窗口内的灰度值最大值最小值，可以使用单调队列维护，这样做时间复杂度与均值阈值中的方法一致，基本可以不用考虑因此而增加的复杂度。&lt;/p&gt;
</content:encoded></item><item><title>智能车逆透视算法（IPM）原理及实现</title><link>https://fuwari.vercel.app/posts/embed/smart-car-ipm/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/embed/smart-car-ipm/</guid><pubDate>Tue, 13 Feb 2024 20:28:30 GMT</pubDate><content:encoded>&lt;p&gt;之前校赛就有在用逆透视了，不过都是硬套学长写好的公式乱写。寒假重新来梳理一下这个算法。参考的是opencv库的函数，例如&lt;code&gt;getPerspectiveTransform&lt;/code&gt;（&lt;a href=&quot;https://github.com/opencv/opencv/blob/bf5a01e297c863f230c9829bb99c4fe59fb30e61/modules/imgproc/src/imgwarp.cpp#L3434&quot;&gt;opencv/modules/imgproc/src/imgwarp.cpp Line 3432&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;写得有点乱，可能一些地方会有错误，不过大致意思应该没问题，代码跑出来效果还可以()。&lt;/p&gt;
&lt;p&gt;和网上其他一些佬的做法应该有所不同，目前比较广为流传的标定方法是四点标定法，通过原图和逆透视后的图，用四个点(x,y)，总计8个参数，调用opencv库进行标定，不方便在下位机中直接调参。因此我改成只用3个参数（起始行$y_u$、平行赛道斜率$k$、赛道起始点列数$y_l$）进行标定，并且3个参数和物理世界有比较直接的对应关系，调整摄像头角度时可以直接调整$k$即可；如果想增加前瞻，就调整$y_u$；如果更改了摄像头高度，调整$y_l$。&lt;/p&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;h2&gt;一. 基本数学模型&lt;/h2&gt;
&lt;p&gt;透视变换可以用以下的公式表示。设$(x,y)$是原图像中的点，$(x&apos;,y&apos;)$是对应的俯视图中的点（我们先假设这个是存在对应的，实际上有的时候并不存在这个对应）。&lt;/p&gt;
&lt;p&gt;再设$M$是（正）透视矩阵，有：&lt;/p&gt;
&lt;p&gt;$$
M\cdot \begin{pmatrix}
x \
y \
z
\end{pmatrix} =\begin{pmatrix}
x&apos; \
y&apos; \
z&apos;
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;一般来说，$z=1$，这使得三维齐次坐标转为二维平面上的位置，也就是说，有&lt;/p&gt;
&lt;p&gt;$$
M\cdot \begin{pmatrix}
x \
y \
1
\end{pmatrix} =\begin{pmatrix}
x&apos;&apos; \
y&apos;&apos; \
w&apos;
\end{pmatrix} =w&apos;\begin{pmatrix}
x&apos; \
y&apos; \
1
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;这个$M$是一个$3\times 3$的矩阵：&lt;/p&gt;
&lt;p&gt;$$
M=\begin{pmatrix}
a_{11} &amp;amp; a_{12} &amp;amp; a_{13} \
a_{21} &amp;amp; a_{22} &amp;amp; a_{23} \
a_{31} &amp;amp; a_{32} &amp;amp; a_{33}
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;变换后的坐标$(x&apos;,y&apos;)$需要通过$w&apos;$进行归一化，也就是说要：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;x&apos;=\frac{a_{11}x+a_{12}y+a_{13}}{a_{31}x+a_{32}y+a_{33}}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;y&apos;=\frac{a_{21}x+a_{22}y+a_{23}}{a_{31}x+a_{32}y+a_{33}}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h2&gt;二. 道路模型的特殊化&lt;/h2&gt;
&lt;h3&gt;特殊化1：$a_{33}=1$&lt;/h3&gt;
&lt;p&gt;在道路模型中，我们通常直接设置$a_{33}=1$，也就是&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;M=\begin{pmatrix}
a_{11} &amp;amp; a_{12} &amp;amp; a_{13} \
a_{21} &amp;amp; a_{22} &amp;amp; a_{23} \
a_{31} &amp;amp; a_{32} &amp;amp; 1
\end{pmatrix}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;这样做是为了数学上便于处理。这样设置后，我们可以直接得到&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;x&apos;=\frac{a_{11}x+a_{12}y+a_{13}}{a_{31}x+a_{32}y+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;y&apos;=\frac{a_{21}x+a_{22}y+a_{23}}{a_{31}x+a_{32}y+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h3&gt;特殊化2：行坐标固定映射&lt;/h3&gt;
&lt;p&gt;基本的数学模型已经确立了，下面我们要做的是求解这个M矩阵及其逆矩阵。不过在此之前还可以继续进行一些特殊化假设，例如，在畸变较小的相机中，对于同一行坐标$x$的像素值，我们可以认为，它们映射过去后，行坐标$x&apos;$也是同样的。这个假设其实是为了简化后面的图像处理（例如，我们可以通过俯视图的行坐标来估计点的距离，不过这个前提是摄像头得摆正）。&lt;/p&gt;
&lt;p&gt;在前面我们已经得到了公式&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;x&apos;=\frac{a_{11}x+a_{12}y+a_{13}}{a_{31}x+a_{32}y+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;基于这个假设，我们可以得到$x&apos;$值与$y$无关，我们直接可以得到：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\left{ \begin{array}{l}
a_{12}=0 \
a_{32}=0 \
\end{array} \right.&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;（注意，我们这里是以x作为行坐标，如果在一些文献中，以y作为行坐标，那就是$a_{21}$和$a_{31}$等于0了）&lt;/p&gt;
&lt;p&gt;带入后得到：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;x&apos;=\frac{a_{11}x+a_{13}}{a_{31}x+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;y&apos;=\frac{a_{21}x+a_{22}y+a_{23}}{a_{31}x+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h3&gt;特殊化3：梯形斜率对称性&lt;/h3&gt;
&lt;p&gt;在求解矩阵$M$及其逆矩阵前，我们要先进行一个标定。逆透视实际上就是研究正视图中的梯形转为俯视图中的矩形，因此我们首先要得到这个梯形和矩形的分别的四个点坐标及其对应点（起码opencv中是这样做的）。&lt;/p&gt;
&lt;p&gt;但是考虑到调车的时候，如果每次调整摄像头位置都需要用坐标点去标定，不太方便，我们可以进一步考虑赛道的特殊情况。&lt;/p&gt;
&lt;p&gt;如果认为行坐标是$x$，纵坐标是$y$，逆透视实际上就是研究正视图中的梯形转为俯视图中的矩形，因此我们首先要得到这个梯形和矩形的分别的四个点坐标及其对应点，但是这个原图中的梯形的两个侧边如果其中一条斜率是$k$($dy/dx$)，那么另一条就可以表示为$-k$，同时两个侧边延长后必交于图形的纵向中线，也就是$y=mid$这一列。$y=mid$和斜向线的交点的行坐标记为$x_m$。对应的，逆透视后的结果中两条侧边应该成为$k&apos;=0$（是纵向直线了，$dy=0$了），相对应的$x_m&apos;$应该成为无穷。&lt;/p&gt;
&lt;p&gt;很容易从公式$x&apos;=\frac{a_{11}x+a_{13}}{a_{31}x+1}$看出，分母为0的时候，$x_m&apos;$为无穷&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;x_m=-\frac{1}{a_{31}}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;按照这个思路输入，那么标定时需要的参数就相对直观且易于调整了，我们只需要如下参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;$k$&lt;/th&gt;
&lt;th&gt;两条赛道（梯形侧边线）的斜率，这里假设是大于0的。&lt;/th&gt;
&lt;th&gt;自标定&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$y_l$&lt;/td&gt;
&lt;td&gt;左边线与原图片下边界的交点的列坐标。 （左边线有可能与下边界没有交点，为了方便我们假设这个交点存在。如果这个交点不存在，那说明摄像头稍微有点太低了，可以把摄像头位置调高一点，或者把假设的下边界调高一点）&lt;/td&gt;
&lt;td&gt;自标定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$H$&lt;/td&gt;
&lt;td&gt;原图下边界的行坐标，也就是原图的高度。&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$C$&lt;/td&gt;
&lt;td&gt;原图的宽度，也就是列坐标最大值+1。要引入这个值，是因为y_r（右边线与图片下边界的交点的列坐标），满足y_r=C-y_l-1。&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$W$&lt;/td&gt;
&lt;td&gt;目标结果图的高度和宽度。为了方便我一般假设目标结果图是正方形，这样高度和宽度就一致了。如果不是正方形就稍微按照这个思路加一个参数就行。一般我设置成120px。&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$W_R$&lt;/td&gt;
&lt;td&gt;目标结果图中的赛道宽度，一般来讲我设成40px或者45px，因为赛道宽度是45cm，减去两侧黑胶带是40cm，这样方便换算。&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$y_u$&lt;/td&gt;
&lt;td&gt;目标结果图中最远处在原图中的横坐标，这个点不能高于灭点。（一般来讲灭点是高于水平中线的，也就是说小于最大行坐标的一半，一般来说我会象征性地设成灭点+15px）&lt;/td&gt;
&lt;td&gt;自标定&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看出，在调整摄像头位置和角度的时候，我们只需要调整$k$、$y_l$和$y_u$这3个参数即可，其余都是硬件和算法固有参数，设好了可以不动。比起普通的用四个坐标xy映射前后对应（总共16个参数）来标定，这样标定在调试的时候更快。&lt;/p&gt;
&lt;p&gt;（注意，$y_l$是列坐标，$y_u$是行坐标，我写的有点乱，不过意思应该没错。）&lt;/p&gt;
&lt;h2&gt;三. 逆透视矩阵参数标定&lt;/h2&gt;
&lt;p&gt;如下图，通过这样写，直接写好程序，调试时就可以比较方便地不通过上位机、在下位机上更改逆透视参数了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIiKyt&quot;&gt;&amp;lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIiKyt.png&quot; alt=&quot;pkIiKyt.png&quot; style=&quot;zoom: 50%;&quot; /&amp;gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下位机程序中，只需要调整这三个参数即可修改逆透视参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 设置的参数 */
float IPM_ROAD_K; // 标定的赛道斜率 j=ki+b 【这个值一般越大，摄像头角度越低，越接近于完全正对地面】
int IPM_ROAD_L_START; // 赛道开始坐标(IMG_ROWS-1, START) 【这个和摄像头高度有关，没有调高度，这个应该不用调的】
int IPM_FAREST_ROW; // 逆透视最远行在原图的行坐标
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这两个参数和算法预先设置的参数，可以算出矩阵$M$的所有参数。下面是我的一种计算方法，opencv库&lt;code&gt;getPerspectiveTransform&lt;/code&gt;函数的写法则是另一种适应更多情况的解法，我的方法是由智能车中特殊化的条件，简化了一些部分。&lt;/p&gt;
&lt;p&gt;首先从前面（特殊化1和特殊化2）我们已经推导得到，$a_{12}=a_{32}=0$，$a_{33}=1$，只剩下：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;M=\begin{pmatrix}
a_{11} &amp;amp; 0 &amp;amp; a_{13} \
a_{21} &amp;amp; a_{22} &amp;amp; a_{23} \
a_{31} &amp;amp; 0 &amp;amp; 1
\end{pmatrix}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;然后前面从梯形斜率对称性（特殊化3）我们推导得到：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;a_{31}=-\frac{1}{x_m}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;其中$x_m$是斜线与纵向中线的交点，可以通过几何关系由$k$和$y_l$表达出来。需要注意的是因为$x_m$计算出来的可能小于0，是正常的，但是代码里不能用&lt;code&gt;uint8&lt;/code&gt;存这个变量，可以用&lt;code&gt;int&lt;/code&gt;或者&lt;code&gt;float&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;$$
\begin{align}
a_{31} &amp;amp;= -\frac{1}{x_m} \
&amp;amp;= -\frac{1}{\frac{C-2\cdot y_l}{2k}+H-1}
\end{align}
$$&lt;/p&gt;
&lt;p&gt;现在来看进一步的标定。有一个问题就是，一般来讲，我们希望俯视图中横纵的每个像素表示的距离是一样的，这样比较方便。我前面已经设定了目标结果图中的赛道宽度是$W_R=40px$，也就是横向上1px约等于1cm，那我也希望在纵向上也实现这样的比例。实现这个可以直接在地上测量标定，但是我想试一下，能不能不通过二次标定实现这个结果？&lt;/p&gt;
&lt;p&gt;（这个问题我还没有解决，目前还在用物理标定来解决，汗流浃背了。不过我感觉是可以根据灭点距离水平中线的比例尺结合相机焦距来算的，因为相机平放正对前方的时候，灭点一般处于水平中线上。）&lt;/p&gt;
&lt;p&gt;先不谈这个问题，直接来用预估的目标结果图中最远处在原图中的横坐标$y_u$计算。在前面已经写了，原图和结果图的行坐标是一一对应的，因为我们这里用的相机畸变很小。也就是说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原图&lt;/strong&gt;  -&amp;gt;  &lt;strong&gt;结果图&lt;/strong&gt;
$y_u$  -&amp;gt;  0
(H-1)  -&amp;gt;  (W-1)&lt;/p&gt;
&lt;p&gt;由于我们前面已经求解得到了$a_{31}$，那么方程$x&apos;=\frac{a_{11}x+a_{13}}{a_{31}x+1}$中就剩下两个未知数$a_{11}$和$a_{13}$了，整理一下，求解这个线性方程组即可：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\begin{bmatrix}
y_u &amp;amp; 1 \
H-1 &amp;amp; 1
\end{bmatrix}
\begin{bmatrix}
a_{11} \
a_{13}
\end{bmatrix}
=\begin{bmatrix}
0 \
(W-1)(a_{31}(H-1)+1)
\end{bmatrix}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;对应这种n元方程组，可以跑个高斯消元算法求解，不过我们已经将维度降到2维了，完全没必要。我们将$(W-1)(a_{31}(H-1)+1)$设成常数$T$，直接给出$a_{11}$和$a_{13}$公式：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\left{ \begin{array}{l}
a_{11}=\frac{-T}{y_u-(H-1)} \
a_{13}=\frac{y_u\cdot T}{y_u-(H-1)} \
\end{array} \right.&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;接下来来看$a_{21}$、$a_{22}$、$a_{23}$的求解，可以列个三元方程解，不过也可以利用一些性质。假设有两个处于同一行、且关于纵向中线对称的两个像素点$(x,y_1)$、$(x,y_2)$，它们在原图中和结果图中满足&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\left{ \begin{array}{l}
y_1+y_2=C-1 \
y_1&apos;+y_2&apos;=W-1 \
\end{array} \right.&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;带入到公式$y&apos;=\frac{a_{21}x+a_{22}y+a_{23}}{a_{31}x+1}$中，得到：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;W-1=\frac{2a_{21}x+a_{22}(C-1)+2a_{23}}{a_{31}x+1}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;需要注意，因为我们每一行都满足这个公式，也就是说$x$为任意值时都成立，进一步得到：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;a_{21}=\frac{1}{2} a_{31}(W-1)&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_{22}(C-1)+2a_{23}=W-1&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;$a_{22}$和$a_{23}$还需要再加一个方程求解，直接带入原图$(H-1,y_l)$-&amp;gt;结果图$(W-1, W/2-W_R/2)$的映射，得到二元方程组：&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;\left{ \begin{array}{l}
(C-1)a_{22}+2a_{23}=W-1 \
y_l\cdot a_{22}+a_{23}=\frac{1}{2}(W-W_R)(a_{31}(W-1)+1)-a_{21}(W-1)
\end{array} \right.&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;p&gt;同样和上面一样，设$T_2=\frac{1}{2}(W-W_R)(a_{31}(W-1)+1)-a_{21}(W-1)$，解得&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{array}{l}
a_{22}=\frac{(W-1)-2\cdot T_2}{(C-1)-2\cdot y_l} \
a_{23}=\frac{(C-1)\cdot T_2-(W-1)\cdot y_l}{(C-1)-2\cdot y_l}
\end{array} \right.&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;p&gt;结果图：弯道&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIi3TS&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIi3TS.png&quot; alt=&quot;pkIi3TS.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;结果图：十字&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgse.com/i/pkIi1w8&quot;&gt;&lt;img src=&quot;https://s21.ax1x.com/2024/07/15/pkIi1w8.png&quot; alt=&quot;pkIi1w8.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;五. 代码实现&lt;/h2&gt;
&lt;p&gt;作为全局变量供上位机设置的参数主要有三个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 设置的参数 */
float IPM_ROAD_K;		// 标定的赛道斜率 j=ki+b	【这个值一般越大，摄像头越低】
int IPM_ROAD_L_START;	// 赛道开始坐标(IMG_ROWS-1, START) 【这个和摄像头高度有关，没有调高度，这个应该不用调的】
int IPM_FAREST_ROW;		// 逆透视最远行在原图的行坐标
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IPM.h&lt;/code&gt;提供给其他文件的接口只需要这几个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void IPM_init(void);		// 逆透视初始化（全局只需要预处理一次，如果重新设置了参数，需要重新调用）
#define ARL_ROW(x,y) aerial_row[x]		// 原图中的(x,y)对应的俯视图行数
#define ARL_COL(x,y) aerial_col[x][y]	// 原图中的(x,y)对应的俯视图列数
#define FNT_ROW(x,y) front_row[x]		// 俯视图中的(x,y)对应的原图行数
#define FNT_COL(x,y) front_col[x][y]	// 俯视图中的(x,y)对应的原图列数
#define isView(x,y)	(0&amp;lt;=x&amp;amp;&amp;amp;x&amp;lt;IPM_WIDTH&amp;amp;&amp;amp;View_lim_L[x]&amp;lt;=y&amp;amp;&amp;amp;y&amp;lt;=View_lim_R[x])	// 俯视图中(x,y)点是否可见
#define View(x,y)	ImageGray[front_row[x]][front_col[x][y]]				// 俯视图中(x,y)点灰度值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其余的固定参数和用到的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 固定参数 */
#define IPM_ROAD_WIDTH (40)	// 结果图中的赛道宽度
/* 逆透视矩阵 */
float IPM_MAT[4][4];	// 逆透视矩阵M，其实是3*3，0位不用
float IPM_INV[4][4];	// M的逆矩阵
#define a IPM_MAT
#define b IPM_INV
/* 预处理好的值 */
uint8 aerial_row[IMG_ROWS];	// 原图中第i行对应的俯视图行数
uint8 front_row[IPM_WIDTH];	// 俯视图中第i行对应的原图行数
uint8 aerial_col[IMG_ROWS][IMG_COLS];	// 原图中(i,j)对应的俯视图(u,v)的列数v
uint8 front_col[IPM_WIDTH][IPM_WIDTH];	// 俯视图中(u,v)对应的原图(i,j)的列数j
uint8 View_lim_L[IPM_WIDTH];			// 俯视图中第i行的列数左限制（可以等于）
uint8 View_lim_R[IPM_WIDTH];			// 俯视图中第i行的列数右限制（可以等于）
uint8 ImageIPM[IPM_WIDTH][IPM_WIDTH];	// 逆透视灰度数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的主要函数的实现如下。&lt;/p&gt;
&lt;p&gt;逆透视矩阵计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * 函数功能: 逆透视矩阵计算
  * 传入参数: 无
  * 传出参数: 无
  * 备注:	 根据k和start，计算矩阵IPM_MAT
  */
void IPM_matrix_calculate(void) {
    static float x_m;
    static float T;
    static float T2;
    /* IPM_MAT 计算 */
    a[1][2] = a[3][2] = 0.0f;
    a[3][3] = 1.0f;
    x_m = IMG_ROWS - 1 + ((IMG_COLS &amp;gt;&amp;gt; 1) - IPM_ROAD_L_START) / (-IPM_ROAD_K);
    a[3][1] = -1.0f / x_m;
    // 下面两个不一定对
    T = (IPM_WIDTH - 1) * (a[3][1] * (IMG_ROWS - 1) + 1);
    a[1][1] = -T / (IPM_FAREST_ROW - IMG_ROWS + 1);
    a[1][3] = IPM_FAREST_ROW * T / (IPM_FAREST_ROW - IMG_ROWS + 1);
    a[2][1] = 0.5f * a[3][1] * (IPM_WIDTH - 1);
    T2 = (IPM_WIDTH - IPM_ROAD_WIDTH) * (a[3][1] * (IPM_WIDTH - 1) + 1.0f) * 0.5f - a[2][1] * (IPM_WIDTH - 1);
    a[2][2] = ((IPM_WIDTH - 1) - 2.0f * T2) / (IMG_COLS - 1 - (IPM_ROAD_L_START &amp;lt;&amp;lt; 1));
    a[2][3] = ((IMG_COLS - 1) * T2 - (IPM_WIDTH - 1) * IPM_ROAD_L_START) / (IMG_COLS - 1 - (IPM_ROAD_L_START &amp;lt;&amp;lt; 1));
#if ENVIRONMENT_PC
    CString string = &quot;&quot;;
    string.Format(&quot;MAT \n&quot;); 
    PrintDebug(string);
    for (int i = 1; i &amp;lt;= 3; i++) {
        string.Format(&quot;%.3f\t %.3f\t %.3f\t \r \n&quot;, a[i][1], a[i][2], a[i][3]);
        PrintDebug(string);
    }
#endif
    /* IPM_INV 计算：是IPM_MAT的逆矩阵 */
    // 计算行列式
    float D = a[1][1] * (a[2][2] * a[3][3] - a[2][3] * a[3][2])
        - a[1][2] * (a[2][1] * a[3][3] - a[2][3] * a[3][1])
        + a[1][3] * (a[2][1] * a[3][2] - a[2][2] * a[3][1]);
    if (D != 0) {
        float invDet = 1.0f / D;
        // 计算逆矩阵
        IPM_INV[1][1] = invDet * (a[2][2] * a[3][3] - a[2][3] * a[3][2]);
        IPM_INV[1][2] = invDet * (a[1][3] * a[3][2] - a[1][2] * a[3][3]);
        IPM_INV[1][3] = invDet * (a[1][2] * a[2][3] - a[1][3] * a[2][2]);
        IPM_INV[2][1] = invDet * (a[2][3] * a[3][1] - a[2][1] * a[3][3]);
        IPM_INV[2][2] = invDet * (a[1][1] * a[3][3] - a[1][3] * a[3][1]);
        IPM_INV[2][3] = invDet * (a[1][3] * a[2][1] - a[1][1] * a[2][3]);
        IPM_INV[3][1] = invDet * (a[2][1] * a[3][2] - a[2][2] * a[3][1]);
        IPM_INV[3][2] = invDet * (a[1][2] * a[3][1] - a[1][1] * a[3][2]);
        IPM_INV[3][3] = invDet * (a[1][1] * a[2][2] - a[1][2] * a[2][1]);
        // IPM_INV[3][3] = 1.0f;
#if ENVIRONMENT_PC
        CString string = &quot;&quot;;
        string.Format(&quot;INV \n&quot;);
        PrintDebug(string);
        for (int i = 1; i &amp;lt;= 3; i++) {
            string.Format(&quot;%.3f\t %.3f\t %.3f\t \r \n&quot;, IPM_INV[i][1], IPM_INV[i][2], IPM_INV[i][3]);
            PrintDebug(string);
        }
#endif
    }
    else {
#if ENVIRONMENT_PC
        PrintDebug(&quot;The matrix is singular and cannot be inverted.\n&quot;);
#else
#endif
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逆透视初始化，即调用上面的矩阵计算，并计算出一些全局调用变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * 函数功能: 逆透视初始化
  * 传入参数: 无
  * 传出参数: 无
  * 备注:	 全局只需要预处理一次，如果修改了设置参数，需要重新调用
  */
void IPM_init(void) {
#if ENVIRONMENT_PC
#if 1  // 上位机中一些我设置好的参数
    IPM_ROAD_K = 1.043f;
    IPM_ROAD_L_START = 2;
    IPM_FAREST_ROW = 45;
#else
    IPM_ROAD_K = 0.4f;
    IPM_ROAD_L_START = 40;
    IPM_FAREST_ROW = 20;
#endif
#else
    // 下位机环境，读取Flash中存储的上次标定数据，略
#endif
    IPM_matrix_calculate();	// 计算逆透视矩阵
    static float clip = 0.5f;	// 感觉好像加上去后因为误差不对应的情况稍微好一点
    // 预处理参数
    for (int i = 0; i &amp;lt; IMG_ROWS; i++)
        aerial_row[i] = (a[1][1] * i + a[1][3]) / (a[3][1] * i + 1.0f) + clip;	
    for (int i = 0; i &amp;lt; IMG_ROWS; i++) {
        for (int j = 0; j &amp;lt; IMG_COLS; j++) {
            aerial_col[i][j] = (a[2][1] * i + a[2][2] * j + a[2][3]) / (a[3][1] * i + 1.0f) + clip;
        }
    }
    for (int i = 0; i &amp;lt; IPM_WIDTH; i++) {
        front_row[i] = (b[1][1] * i + b[1][3]) / (b[3][1] * i + b[3][3]) + clip;
    }
    static int temp;
    for (int i = 0; i &amp;lt; IPM_WIDTH; i++) {
        View_lim_L[i] = 255;
        View_lim_R[i] = 0;
        for (int j = 0; j &amp;lt; IPM_WIDTH; j++) {
            temp = (b[2][1] * i + b[2][2] * j + b[2][3]) / (b[3][1] * i + b[3][2] * j + b[3][3]) + clip;
            if (0 &amp;lt;= temp &amp;amp;&amp;amp; temp &amp;lt; IMG_COLS) {
                front_col[i][j] = temp;
                if (View_lim_L[i] &amp;gt; j)
                    View_lim_L[i] = j;
                if (View_lim_R[i] &amp;lt; j)
                    View_lim_R[i] = j;
            }
            else 
                front_col[i][j] = 255;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>