diff --git a/web/doc/2.2 数组的插入与删除.md b/web/doc/2.2 数组的插入与删除.md new file mode 100644 index 0000000..90786ae --- /dev/null +++ b/web/doc/2.2 数组的插入与删除.md @@ -0,0 +1,451 @@ +# 2.2 数组的插入与删除 + +--- + +## 一、先把数组想成一排固定座位 + +今天我们只学两件事: + +- 往数组里放进一个新元素,这叫**插入**。 +- 从数组里拿走一个元素,这叫**删除**。 + +数组可以想成教室里一排**紧紧挨着的固定座位**,每个位置都不能乱跳。 + +比如下面这个数组: + +| 位置(第几个) | 第1个 | 第2个 | 第3个 | 第4个 | 第5个 | +|:---:|:---:|:---:|:---:|:---:|:---:| +| 下标 | 0 | 1 | 2 | 3 | 4 | +| 内容 | 12 | 18 | 23 | 31 | 40 | + +这里要特别注意: + +- 平时说“第 1 个位置、第 2 个位置”,是按照人的习惯数的。 +- 在 C++ 数组里,下标从 `0` 开始。 + +所以: + +- 第 1 个位置,对应下标 `0` +- 第 3 个位置,对应下标 `2` +- 第 `pos` 个位置,对应下标 `pos - 1` + +> 这节课讲图的时候,我们主要说“第几个位置”;写代码的时候,再换成下标。 + +--- + +## 二、为什么数组的插入和删除要“搬家”? + +数组里的元素是**连续排在一起**的。 + +这就像一排已经坐好的同学: + +- 想在中间插入一个人,就得有人挪一挪,腾出空位。 +- 想删掉中间一个人,就会空出一个洞,后面的人要补上来。 + +所以数组的插入和删除,最关键的不是“放进去”或“拿出来”,而是: + +> **怎样移动其他元素,才能既不丢数据,也不弄乱顺序。** + +这就是本节最重要的地方。 + +--- + +## 三、数组插入:先腾位置,再放新元素 + +### 1. 例子:在第 3 个位置插入 20 + +原数组: + +```text +位置: 1 2 3 4 5 6 +内容: [12] [18] [23] [31] [40] [ ] +``` + +注意:这里第 6 个位置先留空,表示数组容量足够大,可以插入新元素。 + +现在要把 `20` 插入到**第 3 个位置**。 + +插入后应该变成: + +```text +位置: 1 2 3 4 5 6 +内容: [12] [18] [20] [23] [31] [40] +``` + +### 2. 插入时,元素是怎样移动的? + +要想让第 3 个位置空出来,原来第 3、4、5 个位置上的元素都要向后移动一格。 + +但一定要记住: + +> **插入时,要从后往前移。** + +移动过程如下: + +```text +原来: [12] [18] [23] [31] [40] [ ] +第1步: [12] [18] [23] [31] [40] [40] 把 40 往后移 +第2步: [12] [18] [23] [31] [31] [40] 把 31 往后移 +第3步: [12] [18] [23] [23] [31] [40] 把 23 往后移 +第4步: [12] [18] [20] [23] [31] [40] 把 20 放进第 3 个位置 +``` + +### 3. 为什么插入要从后往前移? + +因为如果你从前往后移,前面的数据会把后面的数据盖掉。 + +来看一个错误示范。还是在第 3 个位置插入 `20`: + +```text +原来: [12] [18] [23] [31] [40] [ ] + +错误地从前往后移: +第1步: [12] [18] [23] [23] [40] [ ] +第2步: [12] [18] [23] [23] [23] [ ] +第3步: [12] [18] [23] [23] [23] [23] +``` + +你会发现:原来的 `31` 和 `40` 都不见了! + +原因是: + +- 你先把 `23` 复制到了后面。 +- 后面的位置被改掉以后,再继续搬,就只能搬到已经改坏的数据。 + +所以插入时一定要: + +> **从最后一个元素开始,倒着往前挪。** + +### 4. 插入的步骤口诀 + +可以记成一句话: + +> **先留空,从后搬,最后放。** + +具体就是: + +1. 先保证数组后面有空位置。 +2. 从最后一个元素开始,依次向后移动一格。 +3. 移到目标位置后停下。 +4. 把新元素放进去。 +5. 数组长度 `n` 加 1。 + +### 5. 插入的课堂版 C++ 代码 + +下面的代码约定: + +- 数组现在有 `n` 个元素。 +- 要把 `x` 插入到第 `pos` 个位置。 +- `pos` 从 `1` 开始数。 +- 数组容量足够大。 + +```cpp +for (int i = n; i >= pos; i--) { + a[i] = a[i - 1]; +} +a[pos - 1] = x; +n++; +``` + +你可以这样理解这段循环: + +- `i = n` 时,把最后一个元素搬到新位置。 +- `i = n - 1` 时,把倒数第二个元素往后搬。 +- 一直搬到第 `pos` 个位置为止。 + +--- + +## 四、数组删除:先补空位,再缩短长度 + +### 1. 例子:删除第 3 个位置的元素 + +原数组: + +```text +位置: 1 2 3 4 5 +内容: [ 8] [11] [14] [17] [20] +``` + +现在要删除第 3 个位置上的 `14`。 + +删除后应该变成: + +```text +位置: 1 2 3 4 +内容: [ 8] [11] [17] [20] +``` + +### 2. 删除时,元素是怎样移动的? + +第 3 个位置删掉以后,会空出一个位置。后面的元素要依次往前补。 + +注意: + +> **删除时,要从前往后移。** + +移动过程如下: + +```text +原来: [ 8] [11] [14] [17] [20] +第1步: [ 8] [11] [17] [17] [20] 把 17 往前移 +第2步: [ 8] [11] [17] [20] [20] 把 20 往前移 +最后: [ 8] [11] [17] [20] 长度减 1 +``` + +最后那个多出来的 `20` 虽然还留在原来的地方,但因为数组长度已经减 1,所以它已经**不算数组的一部分了**。 + +### 3. 为什么删除要从前往后移? + +因为空位在前面,应该让后面的元素一个一个补上来。 + +如果你反过来,从后往前移,也会出问题。还是删除第 3 个位置: + +```text +原来: [ 8] [11] [14] [17] [20] + +错误地从后往前移: +第1步: [ 8] [11] [14] [20] [20] +第2步: [ 8] [11] [20] [20] [20] +``` + +原来的 `17` 被弄丢了。 + +所以删除时要记住: + +> **从缺口的后面开始,按顺序往前补。** + +### 4. 删除的步骤口诀 + +可以记成一句话: + +> **先删掉,从前搬,最后减。** + +具体就是: + +1. 找到要删除的位置。 +2. 让后面的元素依次向前移动一格。 +3. 数组长度 `n` 减 1。 + +### 5. 删除的课堂版 C++ 代码 + +下面的代码约定: + +- 数组现在有 `n` 个元素。 +- 删除第 `pos` 个位置上的元素。 +- `pos` 从 `1` 开始数。 + +```cpp +for (int i = pos; i < n; i++) { + a[i - 1] = a[i]; +} +n--; +``` + +你可以这样理解这段循环: + +- `i = pos` 时,把后一个元素补到空位上。 +- `i = pos + 1` 时,再把更后面的元素补上来。 +- 一直补到最后一个元素为止。 + +--- + +## 五、插入和删除放在一起比较 + +| 操作 | 会发生什么 | 正确移动方向 | 为什么 | +|:---:|:---|:---:|:---| +| 插入 | 中间要腾出一个空位 | 从后往前 | 先保护后面的数据,不让它们被覆盖 | +| 删除 | 中间会出现一个缺口 | 从前往后 | 让后面的数据按顺序补上来 | + +再记一遍: + +- **插入:从后往前移** +- **删除:从前往后移** + +这是本节最重要的结论。 + +--- + +## 六、两个最容易忽略的小细节 + +### 1. 插入前,数组要有空位 + +如果数组已经装满了,就不能直接插入。 + +比如数组最多只能放 5 个数,现在已经有 5 个数了,再插入第 6 个,就没有地方放了。 + +所以写程序时,常常会这样做: + +- 开一个更大的数组,比如最多放 `100` 个数。 +- 实际已经用了多少个,用变量 `n` 记录。 + +### 2. 删除后,不是把数“擦掉”,而是把长度减 1 + +数组删除一个元素后,最后那个位置里可能还保留着旧数值,但这已经不重要了。 + +因为程序只看前 `n` 个元素: + +- 删除前看前 `n` 个 +- 删除后只看前 `n - 1` 个 + +所以真正关键的是: + +> **长度变了,数组有效范围也变了。** + +--- + +## 七、特殊情况也要会判断 + +### 1. 在最后面插入 + +如果要在第 `n + 1` 个位置插入,也就是直接接到数组末尾: + +```text +[12] [18] [23] [31] [40] -> [12] [18] [23] [31] [40] [50] +``` + +这时不需要搬动任何元素,直接放进去即可。 + +### 2. 删除最后一个元素 + +如果删除的是最后一个元素: + +```text +[12] [18] [23] [31] [40] -> [12] [18] [23] [31] +``` + +这时也不需要搬动任何元素,只要把长度减 1 就行。 + +--- + +## 八、最容易犯的 5 个错误 + +### 错误 1:插入时移动方向写反 + +结果:后面的数据会被覆盖。 + +### 错误 2:删除时移动方向写反 + +结果:中间的数据会丢失。 + +### 错误 3:忘记更新 `n` + +插入后忘记 `n++`,新元素虽然放进去了,但程序可能不把它算进去。 + +删除后忘记 `n--`,旧数据虽然应该消失了,但程序还会把它输出出来。 + +### 错误 4:把“第几个位置”和“下标”搞混 + +记住: + +- 第 `1` 个位置,对应下标 `0` +- 第 `pos` 个位置,对应下标 `pos - 1` + +### 错误 5:插入时没有预留空间 + +结果:数组越界,程序可能出错。 + +--- + +## 九、一张总图记住本节内容 + +```text +数组插入:先腾空位 + +[12] [18] [23] [31] [40] [ ] + ← ← ← +[12] [18] [20] [23] [31] [40] + + +数组删除:先补缺口 + +[ 8] [11] [14] [17] [20] + → → +[ 8] [11] [17] [20] +``` + +记忆口诀: + +> **插入看后面,从后往前搬;删除看前面,从前往后搬。** + +--- + +## 十、课堂版完整示例 + +下面是一份把“插入”和“删除”都写进去的课堂版程序。为了简单起见,输入位置 `pos` 按“第几个位置”来理解,也就是从 `1` 开始数。 + +```cpp +#include +using namespace std; + +const int MAXN = 105; +int a[MAXN]; +int n; + +void insertValue(int pos, int x) { + for (int i = n; i >= pos; i--) { + a[i] = a[i - 1]; + } + a[pos - 1] = x; + n++; +} + +void deleteValue(int pos) { + for (int i = pos; i < n; i++) { + a[i - 1] = a[i]; + } + n--; +} + +int main() { + cin >> n; + for (int i = 0; i < n; i++) { + cin >> a[i]; + } + + int pos, x; + cin >> pos >> x; + insertValue(pos, x); + + for (int i = 0; i < n; i++) { + cout << a[i] << " "; + } + cout << "\n"; + + cin >> pos; + deleteValue(pos); + + for (int i = 0; i < n; i++) { + cout << a[i] << " "; + } + cout << "\n"; + + return 0; +} +``` + +--- + +## 十一、自己试一试 + +### 1. 观察移动过程 + +把 `9` 插入到下面数组的第 2 个位置,请你把每一步移动都画出来: + +```text +[3] [7] [12] [15] [ ] +``` + +### 2. 观察删除过程 + +删除下面数组的第 4 个位置,请你把每一步移动都画出来: + +```text +[5] [8] [11] [14] [20] +``` + +### 3. 想一想 + +- 为什么插入和删除都可能需要移动很多个元素? +- 如果总是在数组最前面插入,会不会很麻烦? + +如果你能清楚地回答这两个问题,就说明你已经真正理解数组的插入和删除了。 \ No newline at end of file diff --git a/web/doc/3.1 原码反码补码.md b/web/doc/3.1 原码反码补码.md new file mode 100644 index 0000000..210341c --- /dev/null +++ b/web/doc/3.1 原码反码补码.md @@ -0,0 +1,482 @@ +# 3.1 原码、反码与补码 + +--- + +## 一、先搞清楚:计算机为什么只认识 0 和 1? + +你有没有想过,我们平时写下的数字 `5`、`-3`、`100`,计算机根本"看不懂"? + +计算机的本质是**电路**。电路里的导线,要么**通电**,要么**断电**,只有两种状态。科学家就用 `1` 表示通电,用 `0` 表示断电。这就是**二进制**的由来。 + +**类比:** 想象一排电灯开关,每个开关只能「开」或「关」。用这一排开关,我们可以用不同的开/关组合来代表不同的数字——这就是二进制的核心思想。 + +| 十进制 | 二进制 | +|:---:|:------:| +| 0 | `0000` | +| 1 | `0001` | +| 2 | `0010` | +| 3 | `0011` | +| 5 | `0101` | +| 7 | `0111` | + +> **为什么不用十进制?** +> 如果用十进制,每根导线需要精确区分 10 种不同的电压(0V、1V、2V……9V)。电路稍微受到干扰,电压抖动一点,数字就读错了。而二进制只需区分「高电压」和「低电压」两种情况,极难出错,制造成本也低得多。 + +--- + +## 二、正数好说,负数怎么办? + +表示 `5` 很简单,写成 `0101` 就行。但 `-5` 呢? + +计算机存数字时,会分配固定长度的空间,比如 **8位**(8个 0 或 1)。科学家想出了一个办法: + +> **把最左边(最高位)那一位专门用来表示正负,叫"符号位":** +> +> - `0` → 正数 +> - `1` → 负数 + +剩下的 7 位用来表示数值的大小。 + +这就是最朴素的方案——**原码**。 + +--- + +## 三、原码:最直观的方案 + +### 规则 + +- **正数:** 符号位写 `0`,后面写数值的二进制。 +- **负数:** 符号位写 `1`,后面写数值**绝对值**的二进制。 + +### 例子(8位) + +| 十进制 | 原码 | +|:----:|:-----------:| +| `+5` | `0000 0101` | +| `-5` | `1000 0101` | +| `+0` | `0000 0000` | +| `-0` | `1000 0000` | + +直观,容易理解!但…… + +### 原码的两个大麻烦 + +**麻烦一:零有两个写法** + +`+0` 写成 `0000 0000`,`-0` 写成 `1000 0000`,但它们其实是同一个数 `0`!这会让计算机很困惑——判断一个数是不是 `0` 时,要检查两种情况。 + +**麻烦二:加减法会算错** + +用原码做 `5 + (-5)`,把两个原码直接相加: + +``` + 0000 0101 (+5 的原码) ++ 1000 0101 (-5 的原码) +----------- + 1000 1010 ← 这是 -10 的原码,答案错了! +``` + +正确答案应该是 `0`,结果算出了 `-10`。这意味着计算机必须专门写一套特殊的判断逻辑来处理负数加法,硬件电路会变得很复杂。 + +--- + +## 四、反码:改进的尝试 + +### 规则 + +- **正数:** 反码与原码**完全相同**。 +- **负数:** 符号位保持 `1` 不变,其余 7 位**全部翻转**(`0` 变 `1`,`1` 变 `0`)。 + +### 例子(8位) + +| 十进制 | 原码 | 反码 | +|:----:|:-----------:|:-----------:| +| `+5` | `0000 0101` | `0000 0101` | +| `-5` | `1000 0101` | `1111 1010` | + +### 用反码再试试 `5 + (-5)` + +``` + 0000 0101 (+5 的反码) ++ 1111 1010 (-5 的反码) +----------- + 1111 1111 ← 这是反码,对应的真值是 -0 +``` + +结果是 `1111 1111`,这个反码代表 `-0`。虽然不是理想的 `0000 0000`,但至少方向对了——运算逻辑有所改进。 + +### 反码还剩下的问题 + +零**依然有两种写法**: + +- `0000 0000` = `+0` 的反码 +- `1111 1111` = `-0` 的反码 + +问题没有根本解决,只是往前走了一步。 + +--- + +## 五、补码:现代计算机真正使用的方案 + +### 规则 + +- **正数:** 补码与原码**完全相同**。 +- **负数:** 先求反码,再在最末位**加 1**。 + +### 一步步求 `-5` 的补码(8位) + +``` +第一步:写出 -5 的原码 + 1000 0101 + +第二步:符号位不变,其余位取反(得到反码) + 1111 1010 + +第三步:反码末位加 1(得到补码) + 1111 1010 + + 1 + ----------- + 1111 1011 ← 这就是 -5 的补码 +``` + +### 例子(8位) + +| 十进制 | 原码 | 反码 | 补码 | +|:----:|:-----------:|:-----------:|:-------------:| +| `+5` | `0000 0101` | `0000 0101` | `0000 0101` | +| `-5` | `1000 0101` | `1111 1010` | `1111 1011` | +| `+0` | `0000 0000` | `0000 0000` | `0000 0000` | +| `-0` | `1000 0000` | `1111 1111` | `0000 0000` ✓ | + +> **神奇的事情:** `-0` 的反码 `1111 1111` 加 `1`,得到 `1 0000 0000`,共 9 位!但我们只保留 8 位,最高位的 `1` 自动丢弃,结果就是 `0000 0000`。这样正负零合并为同一个编码了! + +### 用补码验证 `5 + (-5)` + +``` + 0000 0101 (+5 的补码) ++ 1111 1011 (-5 的补码) +----------- +1 0000 0000 ← 产生了进位,但只保留低 8 位 + +结果 = 0000 0000 = 0 ✓ 正确! +``` + +### 再验证 `7 + (-3)`(答案应为 4) + +``` + 0000 0111 (+7 的补码) ++ 1111 1101 (-3 的补码) +----------- +1 0000 0100 ← 低 8 位是 0000 0100 = +4 ✓ +``` + +补码让**减法可以用加法来做**,计算机只需要一个加法器就够了,硬件设计大大简化! + +--- + +## 六、三种编码方式总结 + +| | 原码 | 反码 | 补码 | +|:------------:|:-----------:|:-------------:|:---------------:| +| **正数规则** | 符号位 0 + 绝对值 | 同原码 | 同原码 | +| **负数规则** | 符号位 1 + 绝对值 | 原码各位取反(符号位除外) | 反码 + 1 | +| **零的表示** | 两种(正零/负零) | 两种(正零/负零) | 一种(唯一) | +| **能否直接做加减法** | ✗ 不能 | 部分改善 | ✓ 可以 | +| **8位表示范围** | -127 ~ +127 | -127 ~ +127 | **-128 ~ +127** | + +> **补码能多表示一个负数**,原因是消灭了 `-0`,空出来的编码 `1000 0000` 就用来表示 `-128`。 + +--- + +## 七、补码的"逆运算":已知补码求原来的数 + +如果拿到一个**负数的补码**,怎么还原成十进制? + +**方法:对补码再做一次"取反加一"即可。** + +**例:** 已知某数的 8 位补码为 `1111 1011`,求它是多少? + +``` +第一步:末位加 1(取反加一 = 再做一次补码运算) + 1111 1011 →(取反)→ 1000 0100 →(加1)→ 1000 0101 + +第二步:读结果:符号位为 1(负数),数值部分为 0000 0101 = 5 + +结论:1111 1011 这个补码表示的是 -5。 +``` + +--- + +## 八、用 C++ 实现原码、反码、补码的输出 + +学完了手算方法,我们来用 C++ 写一个程序,让计算机自动完成这些步骤。 + +### 核心思路 + +用一个长度为 8 的 `int` 数组来表示 8 位二进制,**下标 0 存最低位,下标 7 存符号位(最高位)**。 + +- **原码**:先把绝对值不断除以 2 取余数,填入数组;再把下标 7 设为符号位(正数为 0,负数为 1)。 +- **反码**:正数与原码相同;负数把除符号位以外的每一位取反(`0` 变 `1`,`1` 变 `0`,即 `1 - bit`)。 +- **补码**:正数与原码相同;负数在反码的基础上加 1,用"逐位进位"模拟加法。 + +### 代码 + +```cpp +#include +using namespace std; + +// 将非负整数 n(0~127)的二进制填入数组 bits[] +// bits[0] 是最低位,bits[7] 是最高位(符号位) +void fillBits(int n, int bits[8]) { + for (int i = 0; i < 8; i++) { + bits[i] = n % 2; + n = n / 2; + } +} + +// 打印 8 位数组,格式为 "XXXX XXXX"(高位在前) +void printBits(int bits[8]) { + for (int i = 7; i >= 0; i--) { + cout << bits[i]; + if (i == 4) cout << " "; + } + cout << endl; +} + +// 输出整数 n(范围 -127 ~ 127)的原码、反码、补码 +void showCodes(int n) { + int original[8]; // 原码 + int inverse[8]; // 反码 + int complement[8]; // 补码 + + if (n >= 0) { + fillBits(n, original); + // 正数:三种编码完全相同 + for (int i = 0; i < 8; i++) { + inverse[i] = original[i]; + complement[i] = original[i]; + } + } else { + // 原码:绝对值的二进制,符号位设为 1 + fillBits(-n, original); + original[7] = 1; + + // 反码:符号位不变,其余位取反 + inverse[7] = 1; + for (int i = 0; i < 7; i++) { + inverse[i] = 1 - original[i]; + } + + // 补码:反码加 1,模拟手动进位 + int carry = 1; + for (int i = 0; i < 8; i++) { + int sum = inverse[i] + carry; + complement[i] = sum % 2; + carry = sum / 2; + } + } + + cout << "原码: "; printBits(original); + cout << "反码: "; printBits(inverse); + cout << "补码: "; printBits(complement); +} + +int main() { + int n; + cin >> n; + + if (n < -127 || n > 127) { + cout << "请输入 -127 到 127 之间的整数!" << endl; + return 0; + } + + cout << "n = " << n << endl; + showCodes(n); + return 0; +} +``` + +### 运行示例 + +输入 `-5`,输出: + +``` +n = -5 +原码: 1000 0101 +反码: 1111 1010 +补码: 1111 1011 +``` + +输入 `5`,输出: + +``` +n = 5 +原码: 0000 0101 +反码: 0000 0101 +补码: 0000 0101 +``` + +### 关键代码讲解 + +**① `fillBits`:把整数转为二进制数组** + +``` +n = 5: + 5 % 2 = 1 → bits[0] = 1, n = 5/2 = 2 + 2 % 2 = 0 → bits[1] = 0, n = 2/2 = 1 + 1 % 2 = 1 → bits[2] = 1, n = 1/2 = 0 + 其余位全为 0 +``` + +这就是"短除法"的代码版本,与手算完全一致。 + +**② `inverse[i] = 1 - original[i]`:取反** + +- `original[i]` 是 0 时,`1 - 0 = 1`; +- `original[i]` 是 1 时,`1 - 1 = 0`。 + +不需要任何特殊符号,普通减法就能完成翻转。 + +**③ 反码加 1 的进位模拟** + +``` + 假设反码是 1111 1010,加 1: + carry = 1 + i=0: 0+1=1, bits=1, carry=0 + i=1: 1+0=1, bits=1, carry=0 + ...(carry 已为 0,后续不变) + 结果:1111 1011 ← 这正是 -5 的补码 +``` + +用 `sum % 2` 取本位,`sum / 2` 取进位,完全模拟了手算的竖式加法。 + +> **想一想:** 为什么范围限制在 `-127` 到 `127`,而不包括 `-128`? +> 因为 `-128` 没有合法的 8 位**原码**(8 位原码最多表示 `-127`),只有**补码**能表示它(`1000 0000`)。你可以尝试修改代码,增加对 `-128` 的特殊处理。 + +--- + +## 九、练习题 + +### 【第一组】进制转换热身 + +1. 将十进制 `13` 转换成二进制。 +2. 将十进制 `25` 转换成二进制。 +3. 将二进制 `0001 0110` 转换成十进制。 + +--- + +### 【第二组】求原码 + +用**8位**二进制写出下列各数的原码: + +4. `+9` +5. `-9` +6. `+20` +7. `-20` +8. `+0` 和 `-0` 的原码分别是什么?它们相同吗? + +--- + +### 【第三组】求反码 + +用**8位**二进制写出下列各数的反码(需要先写原码再推导): + +9. `+9` +10. `-9` +11. `-20` +12. `-1`(提示:`1` 的原码是 `0000 0001`) + +--- + +### 【第四组】求补码 + +用**8位**二进制写出下列各数的补码: + +13. `+9` +14. `-9` +15. `-20` +16. `-1` +17. `-128`(这道题有些特别,想想为什么原码方法不好用?) + +--- + +### 【第五组】补码反推原值 + +已知以下 8 位补码,请判断符号并求出对应的十进制数: + +18. `0000 1010` +19. `1111 1110` +20. `1111 0000` +21. `1000 0000`(提示:这是补码范围内的特殊值) + +--- + +### 【第六组】补码加法验证 + +用补码计算以下各题,并验证结果正确(用 8 位,溢出位丢弃): + +22. `6 + (-2)` +23. `(-6) + (-2)` +24. `10 + (-10)` +25. `(-1) + 1` + +--- + +### 【第七组】思考题 + +26. 为什么计算机不直接使用原码来做加减法?用 `3 + (-3)` 举例说明原码的问题。 +27. 8 位补码最多能表示多少个不同的整数?范围是多少? +28. 如果把 8 位扩展到 **16 位**,用补码表示整数,范围是多少?(规律:$n$ 位补码的范围是 $-2^{n-1}$ 到 $2^{n-1}-1$) + +--- + +## 参考答案 + +**第一组** + +1. `1101` +2. `1 1001`(即 `0001 1001`) +3. `16 + 4 + 2 = 22` + +**第二组** + +4. `0000 1001` +5. `1000 1001` +6. `0001 0100` +7. `1001 0100` +8. 分别是 `0000 0000` 和 `1000 0000`,**不相同**。 + +**第三组** + +9. `0000 1001`(正数不变) +10. `1111 0110` +11. `1110 1011` +12. `1111 1110` + +**第四组** + +13. `0000 1001` +14. `1111 0111` +15. `1110 1100` +16. `1111 1111` +17. `-128` 的 8 位补码为 `1000 0000`(这是规定值,因为补码多表示了这一个负数) + +**第五组** + +18. 符号位 `0`,正数,`= +10` +19. 取反加一:`0000 0001 + 1 = 0000 0010`,`= -2` +20. 取反加一:`0000 1111 + 1 = 0001 0000`,`= -16` +21. `-128`(特殊规定值) + +**第六组** + +22. `0000 0110 + 1111 1110 = 0000 0100 = +4` ✓ +23. `1111 1010 + 1111 1110 = 1111 1000 = -8` ✓ +24. `0000 1010 + 1111 0110 = 0000 0000 = 0` ✓ +25. `1111 1111 + 0000 0001 = 0000 0000 = 0` ✓ + +**第七组** + +26. `3 + (-3)` 用原码:`0000 0011 + 1000 0011 = 1000 0110 = -6`,答案错误。 +27. $2^8 = 256$ 个,范围 `-128` 到 `+127`。 +28. 范围是 `-32768` 到 `+32767`(即 $-2^{15}$ 到 $2^{15}-1$)。 diff --git a/web/doc/3.2 二进制运算.md b/web/doc/3.2 二进制运算.md new file mode 100644 index 0000000..12e64f1 --- /dev/null +++ b/web/doc/3.2 二进制运算.md @@ -0,0 +1,595 @@ +# 3.2 二进制运算 + +--- + +## 一、为什么还要学“二进制运算”? + +在上一节里,我们已经知道了计算机用二进制存数,也知道了补码能把减法变成加法。接下来要学的是: + +- 按位与(AND) +- 按位或(OR) +- 按位非(NOT) +- 按位异或(XOR) +- 左移(`<<`) +- 右移(`>>`) +- 二进制加法 +- 二进制减法 + +这些运算不是“冷知识”,而是程序里常见的底层工具:权限开关、状态压缩、快速比较、数据校验都会用到。 + +--- + +## 二、先记住 6 个位运算 + +设两个二进制位分别是 $a$ 和 $b$,每一位只可能是 0 或 1。 + +### 1. 按位与 AND(符号 `&`) + +规则:只有两位都为 1,结果才是 1。 + +| a | b | a & b | +|:---:|:---:|:-----:| +| 0 | 0 | 0 | +| 0 | 1 | 0 | +| 1 | 0 | 0 | +| 1 | 1 | 1 | + +可以理解为“都同意才通过”。 + +### 2. 按位或 OR(符号 `|`) + +规则:只要有一位是 1,结果就是 1。 + +| a | b | a \| b | +|:---:|:---:|:------:| +| 0 | 0 | 0 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 1 | 1 | 1 | + +可以理解为“有人同意就通过”。 + +### 3. 按位非 NOT(符号 `~`,一元运算) + +规则:0 变 1,1 变 0。 + +| a | ~a | +|:---:|:---:| +| 0 | 1 | +| 1 | 0 | + +### 4. 按位异或 XOR(符号 `^`) + +规则:两位不同为 1,相同为 0。 + +| a | b | a ^ b | +|:---:|:---:|:-----:| +| 0 | 0 | 0 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 1 | 1 | 0 | + +可以理解为“不同就亮灯”。 + +### 5. 左移(符号 `<<`) + +规则:`x << k` 表示所有位向左移动 $k$ 位,右侧补 0。 + +- 在不溢出的情况下,数值相当于乘 $2^k$。 +- 固定 32 位时,左边移出去的高位会被丢弃。 + +例:8 位下 `00101101 << 2 = 10110100`。 + +### 6. 右移(符号 `>>`) + +规则:`x >> k` 表示所有位向右移动 $k$ 位。 + +- 对无符号数,左侧补 0。 +- 对有符号数,很多语言会做“算术右移”(补符号位),所以课堂里先按无符号来理解更稳妥。 + +例:8 位下 `00101101 >> 3 = 00000101`。 + +--- + +## 三、C++ 里位运算的优先级(一定要加括号) + +下面按 cppreference 的顺序,只保留“和计算最相关”的部分(第 5-7、9-15 级): + +| 优先级(高 -> 低) | 运算符 | 含义 | | +|:-----------:|:----------------- |:----- |:---:| +| 5 | `*` `/` `%` | 乘、除、模 | | +| 6 | `+` `-` | 加、减 | | +| 7 | `<<` `>>` | 左移、右移 | | +| 9 | `<` `<=` `>` `>=` | 关系比较 | | +| 10 | `==` `!=` | 相等比较 | | +| 11 | `&` | 按位与 | | +| 12 | `^` | 按位异或 | | +| 13 | `\|` | 按位或 | | +| 14 | `&&` | 逻辑与 | | +| 15 | `\|\|` | 逻辑或 | | + +看 5 个典型例子: + +1) `a + b << 1` +- 实际等价于 `(a + b) << 1` +- 不是 `a + (b << 1)` +2) `x | y & z` +- 实际等价于 `x | (y & z)` +- 不是 `(x | y) & z` +3) `a ^ b & c` +- 实际等价于 `a ^ (b & c)` +- 不是 `(a ^ b) & c` +4) `x + y > z` +- 实际等价于 `(x + y) > z` +- 不是 `x + (y > z)` +5) `a & b == 0` +- 实际等价于 `a & (b == 0)`(因为 `==` 高于 `&`) +- 想判断“按位与结果是否为 0”,应写成 `(a & b) == 0` + +课堂建议:只要一个表达式里混用了两类以上运算符,就主动加括号,别赌记忆。 + +--- + +## 四、位串上的运算:逐位独立进行 + +例如: + +``` +a = 00101101 +b = 00010111 +``` + +逐位计算: + +``` +a & b = 00000101 +a | b = 00111111 +a ^ b = 00111010 +~a = 11010010 +``` + +注意:`~a` 的结果长度和机器位数有关。课堂里我们常固定成 8 位或 32 位来讨论。 + +### 竖式写法示范(像小学列竖式) + +把高位写在左边、低位写在右边,同一列对齐后逐列运算。 + +1) 按位与 `&` + +```text + 0 0 1 0 1 1 0 1 (a) +& 0 0 0 1 0 1 1 1 (b) +---------------------- + 0 0 0 0 0 1 0 1 (a & b) +``` + +2) 按位或 `|` + +```text + 0 0 1 0 1 1 0 1 (a) +| 0 0 0 1 0 1 1 1 (b) +---------------------- + 0 0 1 1 1 1 1 1 (a | b) +``` + +3) 按位异或 `^` + +```text + 0 0 1 0 1 1 0 1 (a) +^ 0 0 0 1 0 1 1 1 (b) +---------------------- + 0 0 1 1 1 0 1 0 (a ^ b) +``` + +4) 左移与右移也可按“横向挪位”理解 + +```text +a : 0 0 1 0 1 1 0 1 +a << 2 : 1 0 1 1 0 1 0 0 (左移两格,右侧补 0) +a >> 3 : 0 0 0 0 0 1 0 1 (右移三格,左侧补 0) +``` + +--- + +## 五、二进制加法与减法 + +### 1. 二进制加法 + +和十进制竖式一样,也是“本位求和 + 进位”。 + +单个位相加规则: + +- $0+0=0$,进位 0 +- $0+1=1$,进位 0 +- $1+0=1$,进位 0 +- $1+1=0$,进位 1 + +如果再加上原来的进位,就变成三数相加。 + +竖式例子(8 位):`00101101 + 00010111` + +```text +进位: 0 0 1 1 1 1 1 + 0 0 1 0 1 1 0 1 ++ 0 0 0 1 0 1 1 1 +---------------------- + 0 1 0 0 0 1 0 0 +``` + +可让学生从最右列开始,逐列写“本列结果位”和“向左进位”。 + +### 2. 二进制减法 + +可以按“借位法”逐位减,也可以用补码思想把减法变加法。 + +在本章练习里,统一按 **32 位无符号整数** 处理: + +- 超过 $2^{32}-1$ 的高位进位丢弃 +- 不足 0 时按 32 位环绕(相当于加上 $2^{32}$) + +竖式例子(8 位):`00101101 - 00010111` + +```text +借位: 0 0 1 1 1 1 0 + 0 0 1 0 1 1 0 1 +- 0 0 0 1 0 1 1 1 +---------------------- + 0 0 0 1 0 1 1 0 +``` + +从最右列开始,若不够减就向左借 1(借 1 相当于当前位加 2)。 + +--- + +## 六、C++:用数组模拟按位运算与加减 + +下面给一份课堂版代码: + +```cpp +#include +using namespace std; + +const int LEN = 32; + +// 把无符号整数转成二进制数组:bits[0] 是最低位 +void toBits(unsigned int x, int bits[]) { + for (int i = 0; i < LEN; i++) { + bits[i] = x % 2; + x /= 2; + } +} + +// 把二进制数组转回无符号整数 +unsigned int toUInt(const int bits[]) { + unsigned int x = 0; + for (int i = LEN - 1; i >= 0; i--) { + x = x * 2 + bits[i]; + } + return x; +} + +// 打印 32 位二进制串(高位在前) +void printBits(const int bits[]) { + for (int i = LEN - 1; i >= 0; i--) { + cout << bits[i]; + } + cout << '\n'; +} + +void bitAnd(const int a[], const int b[], int c[]) { + for (int i = 0; i < LEN; i++) c[i] = a[i] & b[i]; +} + +void bitOr(const int a[], const int b[], int c[]) { + for (int i = 0; i < LEN; i++) c[i] = a[i] | b[i]; +} + +void bitXor(const int a[], const int b[], int c[]) { + for (int i = 0; i < LEN; i++) c[i] = a[i] ^ b[i]; +} + +void bitNot(const int a[], int c[]) { + for (int i = 0; i < LEN; i++) c[i] = 1 - a[i]; +} + +// 32 位无符号加法:溢出进位自动丢弃 +void addBits(const int a[], const int b[], int c[]) { + int carry = 0; + for (int i = 0; i < LEN; i++) { + int sum = a[i] + b[i] + carry; + c[i] = sum % 2; + carry = sum / 2; + } +} + +// 32 位无符号减法:逐位借位 +void subBits(const int a[], const int b[], int c[]) { + int borrow = 0; + for (int i = 0; i < LEN; i++) { + int cur = a[i] - b[i] - borrow; + if (cur >= 0) { + c[i] = cur; + borrow = 0; + } else { + c[i] = cur + 2; + borrow = 1; + } + } +} + +int main() { + unsigned int A, B; + cin >> A >> B; + + int a[LEN], b[LEN], c[LEN]; + toBits(A, a); + toBits(B, b); + + cout << "A = "; printBits(a); + cout << "B = "; printBits(b); + + bitAnd(a, b, c); + cout << "A AND B= "; printBits(c); + + bitOr(a, b, c); + cout << "A OR B= "; printBits(c); + + bitXor(a, b, c); + cout << "A XOR B= "; printBits(c); + + bitNot(a, c); + cout << "NOT A = "; printBits(c); + + addBits(a, b, c); + cout << "A + B = " << toUInt(c) << '\n'; + + subBits(a, b, c); + cout << "A - B = " << toUInt(c) << '\n'; + + return 0; +} +``` + +--- + +## 七、练习题 + +### 【第一组】按位运算热身(进阶版,8 位) + +已知: + +- `a = 00101101` +- `b = 00010111` +1. 求 `a & b`。 +2. 求 `a | b`。 +3. 求 `a ^ b`。 +4. 求 `~a`。 +5. 求 `~b`。 +6. 求 `(a & b) ^ a`。 +7. 求 `(a | b) ^ b`。 +8. 求 `(a ^ b) & a`。 +9. 求 `(a ^ b) | b`。 +10. 求 `(~a) & b`。 +11. 求 `(~b) | a`。 +12. 求 `a & (~b)`。 +13. 求 `(a | b) & (a ^ b)`。 + +再设: + +- `c = 01011000` +- `d = 00110101` +14. 求 `c & d`。 +15. 求 `c | d`。 +16. 求 `c ^ d`。 +17. 求 `(c ^ d) ^ c`。 +18. 求 `(c & d) | (c ^ d)`。 +19. 求 `~(c ^ d)`。 +20. 求 `(~c) ^ d`。 + +再做移位: + +21. 求 `a << 1`(8 位)。 +22. 求 `a << 3`(8 位)。 +23. 求 `a >> 2`(8 位)。 +24. 求 `b >> 1`(8 位)。 +25. 求 `(a << 2) & 0b11111111`(8 位掩码保留)。 +26. 求 `(b << 1) ^ (a >> 2)`(8 位)。 + +--- + +### 【第二组】真值与规律(先判断,再写一句理由) + +27. 对任意位串 `x`,是否总有 `x ^ x == 0`? +28. 对任意位串 `x`,是否总有 `x & x == x`? +29. 对任意位串 `x`,是否总有 `x | x == x`? +30. 对任意位串 `x`,是否总有 `x ^ 0 == x`? +31. 对任意位串 `x`,是否总有 `x ^ FULL_MASK == ~x`?(其中 `FULL_MASK` 指整个位宽全 1) +32. 对任意位串 `x`,是否总有 `(~x) & x == 0`? +33. 对任意位串 `x`,是否总有 `(~x) | x == FULL_MASK`? +34. 对任意非负整数 `x`,是否总有 `(x << 1) == 2 * x`? +35. 对任意非负整数 `x`,是否总有 `(x >> 1) == x / 2`(整除)? +36. 若 `x` 是 2 的幂,是否总有 `x > 0 && (x & (x - 1)) == 0`? + +--- + +### 【第三组】加减法进阶(8 位,丢弃溢出位) + +37. 计算:`00000101 + 00000110` +38. 计算:`11111111 + 00000001` +39. 计算:`00001010 - 00000011` +40. 计算:`00000000 - 00000001` +41. 计算:`10000000 + 10000000` +42. 计算:`01111111 + 00000001` +43. 计算:`01010101 + 00110011` +44. 计算:`00100000 - 00011111` +45. 计算:`00010000 - 00100000` +46. 计算:`10101010 - 01010101` + +--- + +### 【第四组】逆向构造题(更像算法题) + +设 `a = 11001010`,求一个 8 位 `x`,使得: + +47. `a & x == 10001000`(若有多解,写出任意一个) +48. `a | x == 11101110`(若有多解,写出任意一个) +49. `a ^ x == 01100111`(写唯一解) + +再设 `u = 00110110`,`v = 00000110`: + +50. 构造一个 8 位 `y`,使得 `y & u == v`。 +51. 判断是否存在 8 位 `z`,使得 `z | u == v`,若存在给出一个,若不存在说明原因。 + +--- + +### 【第五组】常用位技巧(探究题,重点) + +这一组不要先背公式,先按题目把例子算出来,再归纳。 + +#### A. 探究“最低位 1”(lowbit) + +对下面每个 `x`,先写出二进制,再计算 `-x`(按补码),最后算出 `x & (-x)`: + +52. `x = 12` + +53. `x = 40` + +54. `x = 44` + +55. `x = 72` + +56. 观察 52-55 的结果,归纳一句话:`x & (-x)` 保留了 `x` 的哪一部分? + +#### B. 探究“去掉最低位 1” + +对下面每个 `x`,计算 `x - 1`,再算 `x & (x - 1)`: + +57. `x = 12` + +58. `x = 40` + +59. `x = 44` + +60. `x = 72` + +61. 观察 57-60 的结果,归纳一句话:`x & (x-1)` 对二进制位做了什么变化? + +#### C. 由例子得到“2 的幂”判定 + +分别计算 `x & (x - 1)`,并记录是否为 0: + +62. `x = 1` + +63. `x = 2` + +64. `x = 4` + +65. `x = 8` + +66. `x = 3` + +67. `x = 6` + +68. 根据 62-67,总结:当 `x > 0` 时,什么条件等价于“`x` 是 2 的幂”? + +#### D. 应用题(把结论用起来) + +69. 不用循环,判断 `64` 是否是 2 的幂,并写出关键表达式值。 +70. 不用循环,判断 `72` 是否是 2 的幂,并写出关键表达式值。 +71. 设 `x = 01011000`(8 位),先求 `x & (-x)`,再给出最低位 1 的位置(最低位记第 0 位)。 +72. 设 `x = 00101000`(8 位),先求 `x & (-x)`,再给出最低位 1 的位置(最低位记第 0 位)。 +73. 设 `x = 10000000`(8 位),先求 `x & (-x)`,再给出最低位 1 的位置(最低位记第 0 位)。 +74. 从 `x = 90` 开始,反复执行 `x = x & (x - 1)` 直到变成 0,需要几步?由此得到 `90` 的二进制中有几个 1? +75. 给定偶数 `n`,写出一个表达式快速判断它是否能被 4 整除,并说明对应的“二进制末尾特征”。 + +## 参考答案 + +**第一组** + +1. `00000101` +2. `00111111` +3. `00111010` +4. `11010010` +5. `11101000` +6. `00101000` +7. `00101000` +8. `00101000` +9. `00111111` +10. `00010010` +11. `11101101` +12. `00101000` +13. `00111010` +14. `00010000` +15. `01111101` +16. `01101101` +17. `00110101` +18. `01111101` +19. `10010010` +20. `10010010` +21. `01011010` +22. `01101000` +23. `00001011` +24. `00001011` +25. `10110100` +26. `00100101` + +**第二组** + +27. 是。 +28. 是。 +29. 是。 +30. 是。 +31. 是(全 1 掩码下逐位翻转)。 +32. 是。 +33. 是。 +34. 在不溢出的前提下是。 +35. 是(无符号或非负整数语境下)。 +36. 是。 + +**第三组** + +37. `00001011`(十进制 11) +38. `00000000`(十进制 0) +39. `00000111`(十进制 7) +40. `11111111`(十进制 255) +41. `00000000`(十进制 0) +42. `10000000`(十进制 128) +43. `10001000`(十进制 136) +44. `00000001`(十进制 1) +45. `11110000`(十进制 240) +46. `01010101`(十进制 85) + +**第四组(给一种可行解)** + +47. 可取 `x = 10001100`。 +48. 可取 `x = 01100100`。 +49. 唯一解 `x = 10101101`。 +50. 可取 `y = 11100110`(只要在 `u` 为 1 的位上与 `v` 对齐即可,`u` 为 0 的位任意)。 +51. 不存在,因为按位或不会把 `u` 中的 1 变成 0,而 `u = 00110110` 在第 5、4、2、1 位已有 1,但 `v = 00000110` 在第 5、4 位是 0。 + +**第五组** + +52. `12 = 1100`,`-12`(8 位)是 `11110100`,`12 & (-12) = 0100`(十进制 4) +53. `40 = 101000`,`40 & (-40) = 001000`(十进制 8) +54. `44 = 101100`,`44 & (-44) = 000100`(十进制 4) +55. `72 = 1001000`,`72 & (-72) = 0001000`(十进制 8) +56. 结论:`x & (-x)` 只保留最低位的 1,其余位清零(即 lowbit) +57. `12 & 11 = 1100 & 1011 = 1000`(十进制 8) +58. `40 & 39 = 101000 & 100111 = 100000`(十进制 32) +59. `44 & 43 = 101100 & 101011 = 101000`(十进制 40) +60. `72 & 71 = 1001000 & 1000111 = 1000000`(十进制 64) +61. 结论:`x & (x-1)` 会把最低位 1 消掉,且更低位保持为 0 +62. `1 & 0 = 0` +63. `2 & 1 = 0` +64. `4 & 3 = 0` +65. `8 & 7 = 0` +66. `3 & 2 = 2`(非 0) +67. `6 & 5 = 4`(非 0) +68. 结论:`x > 0 && (x & (x - 1)) == 0` 当且仅当 `x` 是 2 的幂 +69. `64 & 63 = 0`,所以是 2 的幂 +70. `72 & 71 = 64`,非 0,所以不是 2 的幂 +71. lowbit 是 `00001000`(8),最低位 1 在第 3 位 +72. lowbit 是 `00001000`(8),最低位 1 在第 3 位 +73. lowbit 是 `10000000`(128),最低位 1 在第 7 位 +74. `90 -> 88 -> 80 -> 64 -> 0`,共 4 步,所以有 4 个 1 +75. 可用 `(n & 3) == 0`,对应二进制末两位为 `00` diff --git a/web/doc/3.3 高阶位运算.md b/web/doc/3.3 高阶位运算.md new file mode 100644 index 0000000..f10f3fc --- /dev/null +++ b/web/doc/3.3 高阶位运算.md @@ -0,0 +1,1222 @@ +# 3.3 高阶位运算 + +--- + +## 一、这一节要学什么? + +在上一节里,我们已经会了最基础的按位与、按位或、按位异或、取反、左移、右移。接下来要进入真正“能拿来做题”的部分。 + +这一节的重点不是再背几个符号,而是学会把位运算当成一套工具箱来使用。你会看到: + +- 怎么只改某一位、不动别的位; +- 怎么一次性提取一段二进制位; +- 怎么快速找到“最低位的 1”; +- 怎么在 $O(1)$ 时间判断一个数是不是 2 的幂; +- 怎么用异或找“只出现一次”的数; +- 怎么把“选或不选”压成一个整数,枚举所有方案。 + +如果说 3.2 还是“认识工具”,那 3.3 更像是“学会用工具解题”。 + +--- + +## 二、先介绍 `unsigned` + +这一节我们主要讨论“非负整数”的位运算,所以在 C++ 里,优先推荐使用 `unsigned` 类型。 + +原因很简单: + +- 它本来就不表示负数; +- 做位运算时,不容易被“符号位”干扰; +- 像右移这样的操作,课堂里更容易讲清楚。 + +### 1. `unsigned int` 是什么 + +```cpp +unsigned int x; +``` + +它表示无符号整数,也就是只能存 $0$ 和正整数,不能存负数。 + +在大多数 OJ 和常见编译环境里: + +- `int` 通常是 32 位; +- `unsigned int` 也通常是 32 位。 + +区别在于: + +- `int` 要留一部分范围给负数; +- `unsigned int` 把全部 32 位都拿来表示非负数。 + +所以常见范围可以记成: + +- `int`:大约 $-2^{31}$ 到 $2^{31}-1$; +- `unsigned int`:$0$ 到 $2^{32}-1$。 + +如果课堂里讨论的是“32 位二进制串”,那么 `unsigned int` 非常合适。 + +### 2. `unsigned long long` 是什么 + +```cpp +unsigned long long x; +``` + +它也是无符号整数,只是位数更大。常见环境里,它通常是 64 位。 + +所以常见范围可以记成: + +- `unsigned long long`:$0$ 到 $2^{64}-1$。 + +它适合下面这些场景: + +- 位数可能超过 32 位; +- 要写 `1 << k`,而 `k` 可能比较大; +- 需要把更长的二进制状态装进一个整数。 + +例如: + +```cpp +unsigned long long x = 1ull << 40; +``` + +这里如果你写成 `1 << 40`,就很危险,因为 `1` 默认是 `int`,位宽不够。写成 `1ull` 才表示“这是一个 64 位无符号整数 1”。 + +### 3. 什么时候用哪一个? + +- 只讨论 32 位以内的普通位运算题:优先用 `unsigned int`。 +- 可能涉及更高位,或者要安全地写较大的移位:优先用 `unsigned long long`。 + +课堂里可以先把这条经验记住: + +> 32 位用 `unsigned int`,64 位用 `unsigned long long`。 + +### 4. 输入和输出怎么写 + +#### `unsigned int` + +```cpp +unsigned int x; +cin >> x; +cout << x << '\n'; +``` + +#### `unsigned long long` + +```cpp +unsigned long long x; +cin >> x; +cout << x << '\n'; +``` + +和普通整数一样直接输入输出就可以。 + +### 5. 为什么这一节更推荐 `unsigned` + +例如下面这个式子: + +```cpp +x >> 1 +``` + +如果 `x` 是无符号数,我们就可以直接把它理解成“所有二进制位整体右移一格,左边补 0”。 + +如果 `x` 是有符号数,右移时很多语言还会涉及“符号扩展”,课堂理解会麻烦得多。 + +所以这一节讨论位运算技巧时,默认主要站在 `unsigned` 的角度来讲。 + +### 6. 顺便提一句 `bitset` + +虽然这一节主线不用 `bitset`,但你以后看题解或写调试代码时,还是会经常见到它。 + +最常见的用法是: + +```cpp +#include +#include +using namespace std; + +int main() { + bitset<8> bs(13); // 把十进制 13 转成 8 位二进制 00001101 + cout << bs << '\n'; // 直接输出整串二进制 + cout << bs.count() << '\n'; // 统计二进制里有几个 1 + cout << bs[0] << '\n'; // 读取最低位 + return 0; +} +``` + +它比较适合做这些事: + +- 把一个整数的二进制形式直接打印出来; +- 快速观察某几位的变化; +- 写一些“看位”的小工具。 + +但是它也有很明显的缺陷,所以这节不把它当主角: + +- 长度必须在编译时写死,不像数组或 `vector` 那样灵活; +- 像 `x & (-x)`、`x & (x - 1)`、`x + 1`、`x - 1` 这类高阶技巧,本质上还是整数运算更直接; +- 若频繁在 `bitset` 和整数之间来回转换,代码会更绕; +- `bs[0]` 是最低位,但输出时最高位在左,初学者很容易看反。 + +所以可以把它理解成一句话: + +> `bitset` 适合“展示二进制”,不太适合替代这一节里的核心整数技巧。 + +--- + +## 三、先建立一个核心概念:掩码 mask + +高阶位运算里最常见的词就是“掩码”。 + +你可以把掩码理解成一张“筛子”: + +- 掩码某一位是 1,表示这一位要参与操作; +- 掩码某一位是 0,表示这一位尽量保持不动,或者被过滤掉。 + +例如,设 + +```text +x = 11010110 +mask = 00001111 +``` + +那么: + +```text +x & mask = 00000110 // 只保留末 4 位 +x | mask = 11011111 // 末 4 位全部变成 1 +x ^ mask = 11011001 // 末 4 位全部翻转 +x & ~mask = 11010000 // 末 4 位全部清零 +``` + +所以,许多位运算题本质上都在做两件事: + +1. 先构造出一个合适的掩码。 +2. 再用 `&`、`|`、`^`、`~` 去操作它。 + +这一节后面几乎所有公式,都可以看成是“掩码 + 位运算”的组合。 + +--- + +## 四、最常用的 4 个单点操作 + +下面默认: + +- 右起最低位编号为第 0 位; +- 第 $k$ 位指的是右起第 $k$ 位; +- 用 `unsigned int x` 表示一个 32 位状态。 + +设 `x` 是原数,那么: + +### 1. 把第 k 位变成 1 + +```cpp +x |= (1u << k); +``` + +原因:`1u << k` 只有第 $k$ 位是 1,和 `x` 按位或以后,第 $k$ 位一定变成 1,其余位不受影响。 + +### 2. 把第 k 位变成 0 + +```cpp +x &= ~(1u << k); +``` + +原因:`1u << k` 只有第 $k$ 位是 1,取反后只有第 $k$ 位是 0。再和 `x` 按位与,第 $k$ 位就会被清成 0,其余位保持不变。 + +### 3. 把第 k 位翻转 + +```cpp +x ^= (1u << k); +``` + +原因:异或 1 会翻转,异或 0 保持不变。 + +### 4. 读取第 k 位是 0 还是 1 + +```cpp +unsigned int bit = (x >> k) & 1u; +``` + +先把第 $k$ 位移到最低位,再和 1 相与,只保留这一位。 + +### 演示 + +设: + +```text +x = 00101101 +``` + +若 `k = 3`,那么第 3 位原本是 1。 + +```text +1u << 3 = 00001000 +x | (1u << 3) = 00101101 // 本来就是 1,不变 +x & ~(1u << 3) = 00100101 // 第 3 位清零 +x ^ (1u << 3) = 00100101 // 第 3 位翻转 +(x >> 3) & 1 = 1 +``` + +### 对应程序 1:单点位编辑器 + +下面程序支持 4 类操作:设置某位、清零某位、翻转某位、查询某位。 + +```cpp +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + unsigned int x; + int q; + cin >> x >> q; // 读入初始状态和操作次数 + + while (q--) { + string op; + int k; + cin >> op >> k; // 读入操作名和位编号 + + if (op == "SET") { + x |= (1u << k); // 把第 k 位设为 1 + } else if (op == "CLEAR") { + x &= ~(1u << k); // 把第 k 位清成 0 + } else if (op == "FLIP") { + x ^= (1u << k); // 把第 k 位翻转 + } else if (op == "TEST") { + cout << ((x >> k) & 1u) << '\n'; // 输出第 k 位当前是 0 还是 1 + } + } + + cout << x << '\n'; // 输出最终状态的十进制值 + return 0; +} +``` + +--- + +## 五、连续区间上的位操作 + +真正的题目里,很多时候不是只改 1 位,而是要改“连续一段”。这时就要学会构造“连续若干个 1”的掩码。 + +### 1. 保留末 k 位 + +```cpp +x & ((1u << k) - 1) +``` + +因为: + +```text +1u << k = 1000...000 (1 后面 k 个 0) +(1u << k)-1 = 0111...111 (低 k 位全是 1) +``` + +例如: + +```text +x = 11010110 +k = 4 +(1u << 4) - 1 = 00001111 +x & 00001111 = 00000110 +``` + +### 2. 把末 k 位全部清零 + +```cpp +x & ~((1u << k) - 1) +``` + +例如: + +```text +x = 11010110 +mask = 00001111 +~mask = 11110000 +x & ~mask = 11010000 +``` + +### 3. 取出第 l 到第 r 位这段二进制 + +假设 $0 \le l \le r$,并且位编号仍然从右往左、从 0 开始。 + +公式: + +```cpp +(x >> l) & ((1u << (r - l + 1)) - 1) +``` + +思路是两步: + +1. 先把第 $l$ 位移到最低位。 +2. 再保留低 $(r-l+1)$ 位。 + +例如: + +```text +x = 11010110 +取第 2 到第 5 位 + +x >> 2 = 00110101 +保留低 4 位 = 00000101 +``` + +所以第 2 到第 5 位组成的数是 `0101`,即十进制 5。 + +### 竖式感受 1:为什么“右移再与掩码”能取出一段位 + +还是以 `x = 11010110`,取第 2 到第 5 位为例: + +```text +x = 1 1 0 1 0 1 1 0 +x >> 2 = 0 0 1 1 0 1 0 1 +mask = 0 0 0 0 1 1 1 1 +------------------------ +结果 = 0 0 0 0 0 1 0 1 +``` + +从右往左看,原来第 2 到第 5 位的内容是 `0101`,右移后它被搬到了最低 4 位,再与上 `1111`,其他位就被全部滤掉了。 + +### 4. 用 y 替换 x 的第 l 到第 r 位 + +公式: + +```cpp +unsigned int updateBits(unsigned int x, unsigned int y, int l, int r) { + unsigned int mask = (1u << (r - l + 1)) - 1; + x &= ~(mask << l); // 先清空这一段 + x |= ((y & mask) << l); // 再把 y 的低若干位填进去 + return x; +} +``` + +例如: + +```text +x = 10000000000 +y = 10101 +l = 2, r = 6 +``` + +替换后得到: + +```text +10001010100 +``` + +这正是“更新二进制位”这类题的标准做法。 + +### 竖式感受 2:区间替换到底做了哪两步 + +设 `x = 11101101`,把第 1 到第 3 位替换成 `y = 010`。 + +```text +x = 1 1 1 0 1 1 0 1 +mask = 0 0 0 0 1 1 1 0 +clear x = 1 1 1 0 0 0 0 1 +y << 1 = 0 0 0 0 0 1 0 0 +------------------------ +结果 = 1 1 1 0 0 1 0 1 +``` + +你可以把“区间替换”理解成两个连续动作: + +1. 先把目标区间擦干净; +2. 再把新内容平移到对应位置后填进去。 + +### 对应程序 2:区间取位与区间替换 + +```cpp +#include +using namespace std; + +unsigned int extractBits(unsigned int x, int l, int r) { + unsigned int mask = (1u << (r - l + 1)) - 1; // 低 len 位全是 1 + return (x >> l) & mask; // 先右移,再保留低位 +} + +unsigned int updateBits(unsigned int x, unsigned int y, int l, int r) { + unsigned int mask = (1u << (r - l + 1)) - 1; // 先得到这一段的基础掩码 + x &= ~(mask << l); // 把 x 的 [l, r] 区间清零 + x |= ((y & mask) << l); // 把 y 的低若干位平移后填进去 + return x; // 返回替换后的结果 +} + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + unsigned int x, y; + int l, r; + cin >> x >> y >> l >> r; // 读入原数、新数和区间端点 + + cout << extractBits(x, l, r) << '\n'; // 输出取出的这一段 + cout << updateBits(x, y, l, r) << '\n'; // 输出区间替换后的结果 + return 0; +} +``` + +> 说明:上面程序默认 `r - l + 1 < 32`。如果可能用到更高位,建议把 `1u` 改成 `1ull`,并把类型换成 `unsigned long long`。 + +--- + +## 六、`x & (-x)`:只保留最低位的 1 + +这是位运算里最经典的高阶技巧之一,也常被叫做 `lowbit(x)`。 + +公式: + +```cpp +x & (-x) +``` + +它的作用是: + +> 只保留 `x` 二进制中最右边的那个 1,其余位全部清零。 + +### 例子 1 + +```text +x = 44 = 101100 +x & (-x) = 000100 = 4 +``` + +说明最低位的 1 在第 2 位,它的权值是 4。 + +### 例子 2 + +```text +x = 72 = 1001000 +x & (-x) = 0001000 = 8 +``` + +### 竖式感受:`x & (-x)` 为什么只剩一个 1 + +以 8 位下 `x = 44` 为例: + +```text +x = 0 0 1 0 1 1 0 0 +~x = 1 1 0 1 0 0 1 1 +~x + 1 = 1 1 0 1 0 1 0 0 (这就是 -x 的补码) +------------------------ +x & (-x) = 0 0 0 0 0 1 0 0 +``` + +最右边那个 1 被单独保留下来,其余位都在按位与时被消掉了。 + +### 为什么成立? + +设一个数的二进制长成这样: + +```text +x = ?????1000...000 +``` + +也就是: + +- 最低位的 1 左边是什么无所谓; +- 最低位的 1 右边全是 0。 + +那么它的相反数 `-x` 在补码里,会恰好保留这一位对应的权值,和 `x` 相与以后,就只剩下这一个 1。 + +你不必每次都重新推补码,记住结论即可: + +```cpp +unsigned int lowbit = x & -x; +``` + +在这里把 `x` 看成无符号整数,重点理解“二进制位模式”就够了。 + +### `lowbit` 有什么用? + +- 找最低位的 1; +- 判断某个数能被多少个 2 连续整除; +- 树状数组里跳步; +- 把一个状态拆成若干个单独的二进制位。 + +--- + +## 七、`x & (x - 1)`:消掉最低位的 1 + +公式: + +```cpp +x & (x - 1) +``` + +它的作用是: + +> 把 `x` 最低位的那个 1 清掉,其他更高位尽量保持不变。 + +### 为什么成立? + +设: + +```text +x = ?????1000...000 +x - 1 = ?????0111...111 +``` + +那么: + +```text +x & (x - 1) = ?????0000...000 +``` + +也就是说,最低位的 1 被清掉了。 + +### 例子 + +```text +x = 90 = 1011010 + +第一次:1011010 & 1011001 = 1011000 = 88 +第二次:1011000 & 1010111 = 1010000 = 80 +第三次:1010000 & 1001111 = 1000000 = 64 +第四次:1000000 & 0111111 = 0000000 = 0 +``` + +共做了 4 次才变成 0,所以 90 的二进制里有 4 个 1。 + +### 竖式感受:`x - 1` 会先改掉哪些位 + +以 8 位下 `x = 90 = 01011010` 为例: + +```text +x = 0 1 0 1 1 0 1 0 +x - 1 = 0 1 0 1 1 0 0 1 +------------------------ +x & (x - 1) = 0 1 0 1 1 0 0 0 +``` + +从右往左看,最低位那个 1 被清掉了,而它右边发生变化的部分在按位与后也全部消失,于是只剩“去掉最低位 1”后的结果。 + +### 用它统计 1 的个数 + +```cpp +int popcount(unsigned int x) { + int cnt = 0; + while (x) { + x &= (x - 1); // 每次清掉一个最低位的 1 + ++cnt; // 记录一共清掉了多少次 + } + return cnt; +} +``` + +循环几次,就有几个 1。 + +### 用它判断 2 的幂 + +如果一个正整数是 2 的幂,那么它的二进制里恰好只有一个 1。 + +所以: + +```cpp +x > 0 && (x & (x - 1)) == 0 +``` + +成立,当且仅当 `x` 是 2 的幂。 + +例如: + +- `8 = 1000`,`8 & 7 = 0`,所以是 2 的幂; +- `12 = 1100`,`12 & 11 = 8`,不是 0,所以不是 2 的幂。 + +### 对应程序 3:`lowbit`、统计 1 的个数、判断 2 的幂 + +```cpp +#include +using namespace std; + +unsigned int lowbit(unsigned int x) { + return x & (~x + 1u); // 只保留最低位的 1 +} + +int popcount(unsigned int x) { + int cnt = 0; + while (x) { + x &= (x - 1); // 每次消掉一个最低位的 1 + ++cnt; // 消掉几次,就说明原来有几个 1 + } + return cnt; +} + +bool isPowerOfTwo(unsigned int x) { + return x > 0 && (x & (x - 1)) == 0; // 正数且只含一个 1 +} + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + unsigned int x; + cin >> x; // 读入一个非负整数 + + cout << lowbit(x) << '\n'; // 输出最低位 1 的权值 + cout << popcount(x) << '\n'; // 输出二进制中 1 的个数 + cout << (isPowerOfTwo(x) ? "YES" : "NO") << '\n'; // 输出是否为 2 的幂 + return 0; +} +``` + +这里有两个细节值得注意: + +- `lowbit(x)` 和 `x & (-x)` 是同一个意思; +- 统计 1 的个数时,循环次数正好等于 1 的个数。 + +--- + +## 八、右边连续的 1 和连续的 0 + +很多位运算题,专门喜欢考“最右边连续的一段”。下面这些式子都很常见。 + +### 1. `x & (x + 1)`:把右边连续的 1 清零 + +例如: + +```text +x = 10101111 +x + 1 = 10110000 +结果 = 10100000 +``` + +也就是最右边那一串连续的 1 全没了。 + +### 竖式感受 1:`x & (x + 1)` 和 `x | (x + 1)` + +以 `x = 10101111` 为例: + +```text +x = 1 0 1 0 1 1 1 1 +x + 1 = 1 0 1 1 0 0 0 0 +------------------------ +x & (x + 1) = 1 0 1 0 0 0 0 0 +x | (x + 1) = 1 0 1 1 1 1 1 1 +``` + +所以: + +- `x & (x + 1)` 会把最右边连续的 1 全清掉; +- `x | (x + 1)` 会把右起第一个 0 变成 1。 + +### 2. `x | (x + 1)`:把右起第一个 0 变成 1 + +例如: + +```text +x = 10101111 +x + 1 = 10110000 +结果 = 10111111 +``` + +右起第一个 0 被点亮了,而它右边那一串 1 本来就是 1。 + +### 3. `x | (x - 1)`:把右边连续的 0 变成 1 + +例如: + +```text +x = 10110000 +x - 1 = 10101111 +结果 = 10111111 +``` + +于是最右边那一串 0 全被填成 1。 + +### 4. `~x & (x + 1)`:取出最低位的 0 + +例如: + +```text +x = 10101111 +~x = 01010000 +x + 1 = 10110000 +结果 = 00010000 +``` + +这表示“最右边的那个 0”的权值。 + +### 竖式感受 2:`x | (x - 1)` 和 `~x & (x + 1)` + +先看 `x | (x - 1)`,取 `x = 10110000`: + +```text +x = 1 0 1 1 0 0 0 0 +x - 1 = 1 0 1 0 1 1 1 1 +------------------------ +x | (x - 1) = 1 0 1 1 1 1 1 1 +``` + +这说明右边连续的 0 被全部填成了 1。 + +再看 `~x & (x + 1)`,取 `x = 10101111`: + +```text +x = 1 0 1 0 1 1 1 1 +~x = 0 1 0 1 0 0 0 0 +x + 1 = 1 0 1 1 0 0 0 0 +------------------------ +~x & (x + 1) = 0 0 0 1 0 0 0 0 +``` + +可以看到,最后只留下了最右边那个 0 对应的位置。 + +### 这些式子有什么用? + +- 做状态转移时修改最右边某一段位; +- 写一些构造题; +- 做二进制规律观察题; +- 快速处理“连续 1”或“连续 0”。 + +先别急着死背,建议自己随手写几个二进制例子验证一下,印象会更深。 + +--- + +## 九、异或的高阶用法 + +异或在基础运算里看起来只是“不同为 1,相同为 0”,但它在算法里特别有力量,因为它满足下面这些重要性质: + +- `a ^ a = 0` +- `a ^ 0 = a` +- 满足交换律和结合律 + +这意味着: + +> 相同的数异或两次会被“抵消掉”。 + +### 1. 找出只出现一次的数 + +如果一个数组里,除了一个数外,其余每个数都出现两次,那么把所有数异或起来,最后剩下的就是那个只出现一次的数。 + +例如: + +```text +12 ^ 7 ^ 9 ^ 12 ^ 7 = 9 +``` + +因为 `12 ^ 12 = 0`,`7 ^ 7 = 0`,只剩 `9`。 + +### 竖式感受:为什么异或能把成对元素消掉 + +还是看: + +```text +12 ^ 7 ^ 9 ^ 12 ^ 7 +``` + +利用交换律和结合律,我们可以先把相同的数挪到一起: + +```text +12 ^ 7 ^ 9 ^ 12 ^ 7 += 12 ^ 12 ^ 7 ^ 7 ^ 9 += 0 ^ 0 ^ 9 += 9 +``` + +所以异或在“找只出现一次的数”这类题里特别好用,因为成对元素会自动抵消。 + +代码: + +```cpp +int singleNumber(const vector& a) { + int ans = 0; + for (int x : a) ans ^= x; // 成对元素会两两抵消 + return ans; // 最后剩下的就是只出现一次的数 +} +``` + +### 2. 找出两个只出现一次的数 + +如果数组里恰好有两个数只出现一次,其他数都出现两次,那么: + +1. 先异或全部元素,得到 `xor_all = p ^ q`; +2. `p` 和 `q` 至少有一位不同,所以 `xor_all` 至少有一位是 1; +3. 取出这最低位的 1:`low = xor_all & -xor_all`; +4. 用这 1 位把所有数分成两组; +5. 两组内分别异或,就能分别得到 `p` 和 `q`。 + +代码: + +```cpp +pair twoSingles(const vector& a) { + int xor_all = 0; + for (int x : a) xor_all ^= x; // 得到两个目标数的异或值 + + int low = xor_all & -xor_all; // 取出它们最低位上不同的那一位 + int p = 0, q = 0; + for (int x : a) { + if (x & low) p ^= x; // 这一组中成对元素抵消后,剩下其中一个目标数 + else q ^= x; // 另一组同理 + } + if (p > q) swap(p, q); // 输出时按从小到大排列 + return {p, q}; +} +``` + +### 3. 不用 `+` 求两数之和 + +这也是一道经典题。 + +思路: + +- `a ^ b` 相当于“不进位加法”; +- `(a & b) << 1` 就是所有进位; +- 不断把“当前和”和“进位”重新相加,直到没有进位。 + +代码: + +```cpp +int addWithoutPlus(int a, int b) { + while (b != 0) { + unsigned int carry = (static_cast(a & b) << 1); // 先算出进位 + a = a ^ b; // 再算不带进位的和 + b = static_cast(carry); // 下一轮继续把进位加进去 + } + return a; +} +``` + +### 对应程序 4:异或工具箱 + +下面程序演示三个常见功能:单异常数、双异常数、无加号求和。 + +```cpp +#include +using namespace std; + +int singleNumber(const vector& a) { + int ans = 0; + for (int x : a) ans ^= x; // 成对出现的数会两两抵消 + return ans; // 最后剩下的就是只出现一次的数 +} + +pair twoSingles(const vector& a) { + int xor_all = 0; + for (int x : a) xor_all ^= x; // 得到两个目标数的异或值 + + int low = xor_all & -xor_all; // 取出二者最低位上不同的那一位 + int p = 0, q = 0; + for (int x : a) { + if (x & low) p ^= x; // 这一组里成对元素抵消,剩下其中一个目标数 + else q ^= x; // 另一组同理 + } + if (p > q) swap(p, q); // 方便输出时从小到大 + return {p, q}; +} + +int addWithoutPlus(int a, int b) { + while (b != 0) { + unsigned int carry = (static_cast(a & b) << 1); // 这一轮产生的进位 + a = a ^ b; // 不进位加法 + b = static_cast(carry); // 下一轮继续把进位加进去 + } + return a; +} + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int n; + cin >> n; // 读入数组长度 + vector a(n); + for (int i = 0; i < n; ++i) cin >> a[i]; // 读入数组元素 + + cout << singleNumber(a) << '\n'; // 输出“只出现一次”的单个数 + + pair ans = twoSingles(a); + cout << ans.first << ' ' << ans.second << '\n'; // 输出两个只出现一次的数 + + int x, y; + cin >> x >> y; // 再读入两个整数,演示无加号求和 + cout << addWithoutPlus(x, y) << '\n'; + return 0; +} +``` + +--- + +## 十、状态压缩:把“选或不选”压成一个整数 + +这已经是竞赛和算法里非常重要的一类位运算应用了。 + +### 1. 什么叫状态压缩? + +如果有 $n$ 个对象,每个对象只有两种状态: + +- 选 / 不选 +- 在 / 不在 +- 开 / 关 + +那么就可以用一个整数的二进制位来表示整个状态。 + +例如有 4 个学生,编号为 0、1、2、3: + +```text +mask = 0101 +``` + +表示: + +- 第 0 位是 1,选了 0 号; +- 第 1 位是 0,没选 1 号; +- 第 2 位是 1,选了 2 号; +- 第 3 位是 0,没选 3 号。 + +### 2. 集合运算和位运算的对应关系 + +设 `A`、`B` 都是状态压缩后的整数,则: + +- 并集:`A | B` +- 交集:`A & B` +- 对称差:`A ^ B` +- 差集(A 去掉 B):`A & ~B` + +### 3. 枚举所有子集 + +如果一共有 $n$ 个元素,那么所有子集一共有 $2^n$ 个,对应的状态正好就是: + +```cpp +for (int mask = 0; mask < (1 << n); ++mask) { + // mask 表示一种选法 +} +``` + +每个 `mask` 都代表一种方案。 + +### 4. 枚举某个集合 S 的所有子集 + +这是更高阶、也更常考的一种写法: + +```cpp +for (int sub = S; ; sub = (sub - 1) & S) { + // sub 是 S 的一个子集 + if (sub == 0) break; +} +``` + +它的作用是: + +> 按从大到小的顺序,把 `S` 的所有子集恰好枚举一遍。 + +### 演示 + +设: + +```text +S = 1101 +``` + +那么它的子集会依次枚举出: + +```text +1101 +1100 +1001 +1000 +0101 +0100 +0001 +0000 +``` + +这些数正好都是 `1101` 的子集状态。 + +### 竖式感受:为什么 `(sub - 1) & S` 能继续跳到下一个子集 + +还是取 `S = 1101`,看前两步: + +```text +sub = 1 1 0 1 +sub - 1 = 1 1 0 0 +(sub - 1) & S = 1 1 0 0 +``` + +继续下一步: + +```text +sub = 1 1 0 0 +sub - 1 = 1 0 1 1 +(sub - 1) & S = 1 0 0 1 +``` + +可以发现: + +- `sub - 1` 会先把最低位的 1 消掉; +- 再和 `S` 按位与,就会把不属于 `S` 的那些位全部抹掉; +- 于是就自然跳到了下一个合法子集。 + +### 对应程序 5:用状态压缩枚举所有选法 + +下面是一个很典型的“子集枚举”程序。 + +题意模型:有 $n$ 个活动,每个活动有一个得分,最多能选若干个,要求总分不超过上限 `limit`,问最大总分是多少。 + +为了方便演示,下面先假设 `n <= 20`。 + +```cpp +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int n, limit; + cin >> n >> limit; // 读入活动个数和分数上限 + vector a(n); + for (int i = 0; i < n; ++i) cin >> a[i]; // 读入每个活动的得分 + + int best = 0; + for (int mask = 0; mask < (1 << n); ++mask) { + int sum = 0; + for (int i = 0; i < n; ++i) { + if ((mask >> i) & 1) { + sum += a[i]; // 第 i 位是 1,说明第 i 个活动被选中 + } + } + if (sum <= limit) { + best = max(best, sum); // 在不超上限的前提下更新最优答案 + } + } + + cout << best << '\n'; // 输出最大可行总分 + return 0; +} +``` + +这个程序就是“状态压缩 + 枚举所有子集”的最经典入门模型。 + +--- + +## 十一、位运算中的常见坑 + +高阶位运算很强,但也很容易出错。下面这些坑必须提前知道。 + +### 1. 混合运算时一定加括号 + +例如: + +```cpp +a & b == 0 +``` + +这不是你想的 `(a & b) == 0`,而是: + +```cpp +a & (b == 0) +``` + +正确写法必须是: + +```cpp +(a & b) == 0 +``` + +### 2. 讨论纯位模式时,优先使用无符号数 + +```cpp +unsigned int x; +``` + +这样可以避开一部分负数右移、符号扩展之类的问题。 + +### 3. 写掩码时尽量写 `1u` 或 `1ull` + +例如: + +```cpp +1u << k +1ull << k +``` + +比 `1 << k` 更稳,因为 `1` 默认是 `int`,移位太高时可能出问题。 + +### 4. 不要拿 `pow(2, k)` 代替 `1 << k` + +`pow` 是浮点运算,既慢又可能有精度问题。 + +如果你想得到 $2^k$,应直接写: + +```cpp +1u << k +``` + +### 5. `k` 不能乱取 + +如果是 32 位整数,`k` 最好控制在 `0` 到 `30` 或 `31` 的合理范围内。超出位宽去移位,会产生未定义行为或不符合预期的结果。 + +### 6. 异或交换知道即可,不建议滥用 + +```cpp +a ^= b; +b ^= a; +a ^= b; +``` + +虽然能交换,但可读性差,还容易在 `a` 和 `b` 指向同一位置时出问题。正常写题,直接 `swap(a, b)` 更好。 + +### 7. `unsigned` 只适合讨论“非负数位模式” + +如果题目本身涉及负数语义,比如有符号数大小比较、带符号右移效果,就不能简单地一股脑全换成 `unsigned`。 + +这一节之所以大量使用 `unsigned`,是因为重点在“观察位”“操作位”“利用二进制规律”。 + +### 8. `unsigned long long` 写移位更安全,但也不是无限大 + +```cpp +1ull << k +``` + +确实比 `1 << k` 安全得多,但它仍然只有 64 位。若 `k` 取得太离谱,依然会出问题。 + +### 9. `bitset` 适合辅助观察,但不适合硬套到所有题里 + +如果你只是想把一个数的二进制打印出来,`bitset` 很方便;但如果题目核心是 `lowbit`、清最低位的 1、连续 0/1 的构造,这些操作直接用整数写通常更短、更清楚。 + +课堂里最容易犯的错,就是为了“看起来高级”而硬把整数题改写成 `bitset` 题,结果把本来简单的规律绕复杂了。 + +--- + +## 十二、课堂速查表 + +下面把这一节最常用的表达式放在一起,方便查阅。 + +| 表达式 | 含义 | +|:--|:--| +| `unsigned int x;` | 适合 32 位无符号位运算 | +| `unsigned long long x;` | 适合 64 位无符号位运算 | +| `x | (1u << k)` | 把第 k 位设为 1 | +| `x & ~(1u << k)` | 把第 k 位设为 0 | +| `x ^ (1u << k)` | 把第 k 位翻转 | +| `(x >> k) & 1u` | 读取第 k 位 | +| `x & ((1u << k) - 1)` | 保留末 k 位 | +| `x & ~((1u << k) - 1)` | 清空末 k 位 | +| `(x >> l) & ((1u << (r - l + 1)) - 1)` | 取出第 l 到第 r 位 | +| `x & (-x)` | 只保留最低位的 1 | +| `x & (x - 1)` | 清掉最低位的 1 | +| `x > 0 && (x & (x - 1)) == 0` | 判断是否为 2 的幂 | +| `x & (x + 1)` | 把右边连续的 1 清零 | +| `x | (x + 1)` | 把右起第一个 0 变成 1 | +| `x | (x - 1)` | 把右边连续的 0 变成 1 | +| `~x & (x + 1)` | 取出最低位的 0 | +| `a ^ a = 0` | 相同的数异或会抵消 | +| `1u << k` | 构造 32 位掩码中的单个 1 | +| `1ull << k` | 构造 64 位掩码中的单个 1 | + +--- + +## 十三、这一节学完后,应该会什么? + +如果你已经能独立完成下面这些事,就说明这节真正学会了: + +- 能说清 `unsigned int` 和 `unsigned long long` 的区别与用法; +- 能写出“设置某一位、清空某一位、翻转某一位、读取某一位”的代码; +- 能用掩码提取某一段二进制位; +- 能解释 `x & (-x)` 和 `x & (x - 1)` 的含义; +- 能写出判断 2 的幂、统计 1 的个数的程序; +- 能用异或解决“只出现一次”的题; +- 能把“选或不选”的问题写成状态压缩。 + +到这里,位运算就不再只是“会算”,而是真正进入“会用”的阶段了。 diff --git a/web/docs.php b/web/docs.php new file mode 100644 index 0000000..3ab59fd --- /dev/null +++ b/web/docs.php @@ -0,0 +1,58 @@ + basename($doc_file), + 'title' => preg_replace('/\.md$/i', '', basename($doc_file)) + ); + } + } +} + +$selected_file = ''; +if (isset($_GET['file'])) { + $selected_file = basename($_GET['file']); +} + +if (empty($selected_file) && !empty($doc_entries)) { + $selected_file = $doc_entries[0]['file']; +} + +$selected_title = '文档'; +$selected_markdown = ''; + +if (!empty($selected_file)) { + $selected_path = realpath($docs_dir . DIRECTORY_SEPARATOR . $selected_file); + $docs_root = realpath($docs_dir); + + if ( + $selected_path !== false && + $docs_root !== false && + strpos($selected_path, $docs_root . DIRECTORY_SEPARATOR) === 0 && + is_file($selected_path) && + preg_match('/\.md$/i', $selected_path) + ) { + $selected_markdown = file_get_contents($selected_path); + if ($selected_markdown === false) { + $selected_markdown = ''; + } else { + $selected_markdown = preg_replace('/^\xEF\xBB\xBF/', '', $selected_markdown); + } + $selected_title = preg_replace('/\.md$/i', '', basename($selected_path)); + } +} + +$show_title = $selected_title . ' - ' . $OJ_NAME; + +require('template/' . $OJ_TEMPLATE . '/docs.php'); +?> \ No newline at end of file diff --git a/web/template/syzoj/docs.php b/web/template/syzoj/docs.php new file mode 100644 index 0000000..3156f92 --- /dev/null +++ b/web/template/syzoj/docs.php @@ -0,0 +1,208 @@ + + + + + +
+ + +
+
+

+ +
+ + +
+ +
没有可显示的文档内容。
+ +
+
+ + + + \ No newline at end of file diff --git a/web/template/syzoj/header.php b/web/template/syzoj/header.php index ad946e1..d206807 100644 --- a/web/template/syzoj/header.php +++ b/web/template/syzoj/header.php @@ -22,7 +22,7 @@ } return $result; } - $url=basename($_SERVER['REQUEST_URI']); + $url=basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); $dir=basename(getcwd()); if($dir=="discuss3") $path_fix="../"; else $path_fix=""; @@ -154,6 +154,8 @@ href="cppreference.php"> + " + href="docs.php"> 文档 " href="ranklist.php">