Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,224 @@
## **C++算法讲义:高精度乘法**
### **第一部分:大数 × 小数**
在学习复杂的“大数 × 大数”之前,我们先解决一个简化版的问题:一个很大的数乘以一个小数(`int` 范围内)。这是我们后续大数 × 大数方法一的基础。
#### **核心思想 (大数 × 小数)**
这个过程和高精度加法极其相似,都是逐位处理。不同之处在于,我们不再是 `A[i] + B[i]`,而是用大数的每一位 `A[i]` 去乘以那个小数 `b`,并且进位不再是 `0` 或 `1`,可以是任意大小的数字。
**算法步骤**:
1. 将大数 `A`(字符串)倒序,方便从个位开始处理。
2. 设置一个进位 `carry = 0`。
3. 遍历倒序后的大数 `A` 的每一位 `A[i]`。
4. 计算 `(A[i] - '0') * b + carry`,得到当前位的乘积结果 `res`。
5. 新结果的当前位是 `res % 10`。
6. 新的进位是 `res / 10`。
7. 循环结束后,如果 `carry` 还有剩余,需要继续处理完。
8. 最后将得到的结果字符串再次反转,即为最终答案。
#### **代码实现 (大数 × 小数)**
```cpp
string mul(string a, int b) {
// 特殊情况:如果乘数是 0 或大数是 "0",结果直接是 "0"
if (b == 0 || a == "0") return "0";
// 反转大数 a,方便从个位开始处理
reverse(a.begin(), a.end());
string ans = "";
int carry = 0; // 进位
// 逐位与 b 相乘
for (int i = 0; i < a.size(); i++) {
// 1. 计算当前位的乘积,并加上上一位的进位
int res = (a[i] - '0') * b + carry;
// 2. 将乘积的个位数拼接到 ans 末尾
ans += to_string(res % 10);
// 3. 更新进位(进位是乘积整除10)
carry = res / 10;
}
// 循环结束后,如果还有进位,需要全部追加到 ans 末尾
while (carry) {
ans += to_string(carry % 10);
carry /= 10;
}
// 反转得到最终结果
reverse(ans.begin(), ans.end());
return ans;
}
```
---
### **第二部分:大数 × 大数**
有了大数 × 小数的解决办法,我们现在可以开始着手去完成大数 × 大数的问题了。
### **方法一:分解相加法 **
#### **核心思想**
还记得乘法分配律吗?`a × (b + c) = a × b + a × c`。我们可以把一个大数拆开来看!
`321 × 45`
`= 321 × (5 + 40)`
`= 321 × 5 + 321 × 40`
我们把一个“大数 × 大数”问题,巧妙地转化成了:
1. 一个“**大数 × 小数**” (`321 × 5`)
2. 另一个“**大数 × 小数**” (`321 × 4`),再在其结果末尾**补0**
3. 最后把这两个结果用“**大数 + 大数**”加起来!
这个思路的精髓在于**复用**。我们只需要实现 `mul(string, int)` 和 `add(string, string)` 这两个函数,就可以组合出 `mul(string, string)` 的功能。
#### **代码实现 (分解相加法)**
```cpp
// 注意:此函数依赖我们之前已经实现好的高精度加法函数, 以及我们上面刚实现的 mul(string, int) 函数
string mul_v1(string a, string b) {
if (a == "0" || b == "0") return "0";
reverse(b.begin(), b.end()); // 反转乘数b,从个位开始处理
string total_ans = "0";
// 遍历 b 的每一位
for (int i = 0; i < b.size(); i++) {
int digit_b = b[i] - '0'; // 1. 取出 b 的当前位数字
string part_ans = mul(a, digit_b); // 2. 调用大数×小数函数,计算部分积
// 3. 补0(移位)。个位(i=0)补0个0,十位(i=1)补1个0...
for (int j = 0; j < i; j++) {
part_ans += '0';
}
total_ans = add(total_ans, part_ans); // 4. 调用“大数+大数”函数,累加到总结果上
}
return total_ans;
}
```
---
### **方法二:竖式模拟法**
这种方法更高效,避免了方法一中频繁的字符串操作和多次相加。
#### **8. 核心思想**
我们回顾一下手算 `123 × 45` 的过程。本质上,我们是在计算所有数位的两两乘积,然后把它们放到正确的位置上。
* `3 × 5` 的结果在**个位**
* `2 × 5` 和 `3 × 4` 的结果都在**十位**
* `1 × 5` 和 `2 × 4` 的结果都在**百位**
* ...
**规律发现**:设被乘数 `A` 的第 `i` 位和乘数 `B` 的第 `j` 位相乘(`i`, `j` 从0开始,代表个位、十位...),其结果 `A[i] × B[j]` 应该累加到最终结果 `C` 的第 `i+j` 位上!
**优化策略**:为了避免复杂的实时进位,我们采用**先累加,后统一进位**的策略。
1. 创建一个足够长的结果数组 `res` (长度为 `n+m`)。
2. 用两层循环,计算所有 `A[i] × B[j]`,并直接累加到 `res[i+j]` 上,此时 `res` 里可能存着大于9的数。
3. 最后,从头到尾遍历 `res` 数组,一次性处理完所有进位。
#### **9. 实例 (123 × 45)**
**存储与初始化**
* `A = {3, 2, 1}` (123倒序)
* `B = {5, 4}` (45倒序)
* `res = {0, 0, 0, 0, 0}` (长度为 3+2=5)
**逐位乘积累加 (不进位)**
| 操作 | 累加位置 | 计算 | res 数组状态 |
| :-------- | :------- | :------- | :------------------- |
| A[0]×B[0] | `res[0]` | `+= 3×5` | `{15, 0, 0, 0, 0}` |
| A[1]×B[0] | `res[1]` | `+= 2×5` | `{15, 10, 0, 0, 0}` |
| A[2]×B[0] | `res[2]` | `+= 1×5` | `{15, 10, 5, 0, 0}` |
| A[0]×B[1] | `res[1]` | `+= 3×4` | `{15, 22, 5, 0, 0}` |
| A[1]×B[1] | `res[2]` | `+= 2×4` | `{15, 22, 13, 0, 0}` |
| A[2]×B[1] | `res[3]` | `+= 1×4` | `{15, 22, 13, 4, 0}` |
**统一处理进位**
| 位置 `i` | `res[i]`(处理前) | `res[i] + carry` | `ans += ...` | 新 `carry` |
| :------- | :--------------- | :--------------- | :----------- | :--------- |
| 0 | 15 | `15 + 0 = 15` | `ans += "5"` | `1` |
| 1 | 22 | `22 + 1 = 23` | `ans += "3"` | `2` |
| 2 | 13 | `13 + 2 = 15` | `ans += "5"` | `1` |
| 3 | 4 | `4 + 1 = 5` | `ans += "5"` | `0` |
| 4 | 0 | `0 + 0 = 0` | `ans += "0"` | `0` |
**清理与输出**
* 处理后得到的 `ans` 是 `"53550"`。
* 去除末尾多余的 '0' -> `"5355"`。
* 反转 -> `"5535"`。正确!
#### **10. 代码实现 (竖式模拟法)**
```cpp
string mul_v2(string a, string b) {
// 特殊情况:如果乘数是 0,结果直接是 0
if (b == "0" || a == "0" ) return "0";
// 反转, 让下标 0 对应个位,1 对应十位...
reverse(b.begin(), b.end());
reverse(a.begin(), a.end());
// 创建结果数组(不处理进位)
// n位乘以m位,积最多是 n+m 位
// 初始全部填充为 0
int n = a.size(), m = b.size();
vector res(n + m, 0);
// 逐位相乘并累加
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int digitA = a[i] - '0'; // 将字符转换为数字进行乘法
int digitB = b[j] - '0';
// a的第i位与b的第j位相乘,放在结果数组的第 i+j 位
res[i + j] += digitA * digitB;
}
}
string ans = ""; // 最终的结果
int carry = 0; // 储存进位,初始化为 0
for (int i = 0; i < res.size(); i++) {
// 计算当前位的数值,开始处理进位
int num = res[i] + carry;
// 计算下一位的进位
carry = num / 10;
// 当前位只保留个位数添加到结果
ans += to_string(num % 10);
}
while(carry){ // 处理最后的进位
ans += to_string(carry % 10);
carry /= 10;
}
// 去除末尾多余的 '0'(即原数的前导零)
while (ans.size() > 1 && ans.back() == '0') {
ans.pop_back();
}
// 翻转回来
reverse(ans.begin(), ans.end());
return ans;
}
```
---
#### **方法对比**
**结论**:在算法竞赛中,**方法二(竖式模拟法)是主要的选择**。方法一则胜在思路巧妙,体现了优秀的模块化和问题分解思想。
---
### 总结
1. 高精度乘法有两种主流思路:**分解相加**和**竖式模拟**。
2. **分解相加**:将 `大数×大数` 转化为多次 `大数×小数` 和 `大数+大数`,核心是**复用**。
3. **竖式模拟**:用 `vector` 存储中间结果,核心是 `C[i+j] += A[i] × B[j]` 公式和**先累加后进位**的思想。
4. 在性能上,**竖式模拟法**优于分解相加法。
@@ -0,0 +1,120 @@
# **C++ 高精度减法讲义**
#### 1. 思想
既然高精度加法可以通过“反转 -> 补零 -> 逐位相加”的流程简化计算,减法是否也可以呢?
答案是肯定的!通过**完全相同的预处理步骤**,我们同样可以将减法问题转化为一个从左到右的、下标对齐的简单循环。
**产生了新的问题:**
我们已经知道如何用字符串来模拟大数的加法。减法同样可以模拟,但比加法多了三个问题:
* **预处理**:减法必须先判断大小,确定结果的符号。
* **计算**:将进位(`carry`) 逻辑替换为借位(`borrow`) 逻辑。
* **后处理**:减法的结果可能产生前导零 (例如 `123-120=003`),需要特殊处理。
#### 2. 算法步骤详解
**判断符号 (这是减法独有的)**
* 在计算前,必须比较 `a` 和 `b` 的大小。
* 创建一个 `bool negative = false;` 标志位。
* 如果 `b` 比 `a` 大,说明结果是负数。我们记下 `negative = true;`,然后交换 `a` 和 `b`。
* **目的**:通过这一步,我们保证了接下来的计算永远是 `大数 - 小数`,极大地简化了借位逻辑。
**反转与补零 (与加法完全一致)**
* 将 `a` 和 `b` (已经是大数和小数了) 同时反转。
* 在较短的字符串**末尾**补 `0`,使其与较长的等长。
* **目的**:让原本的“个位、十位、百位...”对齐到字符串的索引 `0, 1, 2...`,从而可以使用一个简单的 `for` 循环从左到右处理。
**核心循环——逐位相减**
这是算法的核心,我们将在这里实现借位逻辑。
* 创建一个 `int borrow = 0;` 用于记录借位。
* 循环从 `i = 0` 开始:
1. **取数计算**:`diff = (a[i]-'0') - (b[i]-'0') - borrow;`
* `a[i]` 是被减数,`b[i]` 是减数。
* `borrow` 是来自**更低位**的借位请求。如果上一轮(`i-1`)发生了借位,`borrow` 就是 `1`,意味着当前位的 `a[i]` 已经被借走了一个 `1`。
2. **判断并处理借位**:
* 如果 `diff < 0`,说明当前位不够减。
* `diff += 10;` (向更高位借来的 `1` 在本位体现为 `10`)。
* `borrow = 1;` (设置借位标记,告诉**下一轮循环**,它的 `a[i+1]` 被借走了)。
* 如果 `diff >= 0`,说明够减。
* `borrow = 0;` (重置借位标记)。
3. **拼接结果**:`ans += to_string(diff);`
**去除前导零 (在反转前操作)**
* 我们得到的 `ans` 是一个倒序的结果。例如,`123 - 120` 的计算结果 `3`,在 `ans` 中会是 `"300"`。
* 如果直接反转,会得到 `"003"`。
* 所以,我们必须在**最终反转之前**,去除 `ans` **末尾**多余的 `0`。
* **注意**:要保留最后一位 `0` (例如 `10 - 10` 的结果是 `0`),所以循环条件是 `ans.size() > 1`。
**最终反转与添加符号**
* 将清理好的 `ans` 字符串反转回来,得到正确的顺序。
* 检查设置的 `negative` 标志位。如果为 `true`,在结果的最前面加上 `"-"`。
#### 3. 完整代码实现
```cpp
// 比较函数,判断 a 是否大于 b
bool isGreater(string a, string b) {
if (a.length() != b.length()) {
return a.length() > b.length();
}
return a > b; // 长度相同时,直接按字典序比较
}
string sub(string a, string b) {
// 判断符号,并确保 a >= b
bool negative = false;
if (isGreater(b, a)) {
negative = true;
swap(a, b);
}
// 将两个输入字符串都反转
reverse(a.begin(), a.end());
reverse(b.begin(), a.end());
// 在较短的字符串末尾补 '0',使其与较长的等长
while (b.size() < a.size()) b += '0';
string ans = "";
int borrow = 0;
// 核心循环,逐位相减
for (int i = 0; i < a.size(); i++) {
// 计算当前位的差值(考虑上一位的借位)
int diff = (a[i] - '0') - (b[i] - '0') - borrow;
// 判断是否需要向更高位借位
if (diff < 0) {
// 当前位不够减,需要借位
diff += 10; // 借来的 1 在本位变成了 10
borrow = 1; // 产生了借位,标记下一轮计算要被借走
} else {
// 当前位够减,无需借位
borrow = 0; // 重置借位标记
}
// 将当前位的结果拼接到 ans 末尾
ans += to_string(diff);
}
// 在反转前,把倒序结果末尾的 0 去掉 (处理前导零)
while (ans.size() > 1 && ans.back() == '0') {
ans.pop_back();
}
// 将最终结果反转回来
reverse(ans.begin(), ans.end());
// 如果结果为负,添加负号
if (negative) ans = "-" + ans;
return ans;
}
```
@@ -0,0 +1,122 @@
# **C++ 高精度加法**
#### **一:为什么需要高精度计算?**
在编程中,我们常用的 `int`、`long long` 等数据类型都有其表示范围的上限。例如,一个 64 位的 `long long` 最大也只能存储约 `9 x 10^18` 的数字。当我们面临的问题需要处理的数值远超这个范围时——比如计算 100 的阶乘(一个有 158 位的天文数字),或者在密码学中处理超长大整数——标准数据类型便无能为力了。
**高精度计算**,就是为了解决这个问题而生。它利用字符串或数组来模拟我们人类进行笔算的过程,从而处理任意大(只要内存允许)的整数运算。而接下来就会讲解最基础也最重要的运算:**高精度加法**。
#### **二:算法思路**
**1. 方法一:双指针法**
最自然的思路是**模拟小学的竖式计算**:末位对齐,从右到左逐位相加,并处理进位。如果用代码直接实现,通常是这样的:
* 使用两个指针 `i` 和 `j`,分别指向两个字符串 `a` 和 `b` 的末尾。
* 在一个 `while (i >= 0 || j >= 0)` 的循环中进行计算。
* 循环内部需要写复杂的 `if-else` 来处理:`i` 和 `j` 都有效、只有 `i` 有效、只有 `j` 有效这三种情况。
这种方法虽然直观,但**代码实现相对复杂,边界条件多,容易出错**。
**2. 反转对齐法**
为了规避双指针的复杂性,我们引入一种实现更简洁的思路:
**“与其费力地从后往前处理复杂的边界,不如我们先把它变成一个简单的、从头走到尾的问题!”**
这就是“反转对齐法”的核心思想。它通过预处理,将所有情况(如两个数长度不同)统一为一种标准模型,使得最核心的“逐位相加”过程可以用一个极其简单的单循环完成。
#### **三:核心流程**
让我们通过计算 `987 + 56` 来感受这个这个过程:
**原始数字**
* `a = "987"`
* `b = "56"`
**第一步:全部反转**
我们将两个字符串都进行反转,这样它们的个位、十位、百位...就分别对齐到了字符串的索引 `0, 1, 2...`。
* `a` 变为 `"789"`
* `b` 变为 `"65"`
**第二步:补零对齐
为了让两个字符串的每一位都能一一对应相加,我们将较短的字符串在**末尾补 `0`,直到它们等长。
* `a` 保持 `"789"`
* `b` 变为 `"650"`
> 在反转后的字符串**末尾**补 `0`,等价于在原始数字的**开头**补 `0` (例如,`56` 变成 `056`),这是在模拟竖式计算的末位对齐。
**第三步:从左到右逐位相加 **
由于已经反转对齐,我们现在可以从索引 `0` 开始,用一个简单的 `for` 循环来完成所有位的相加。
```
(carry) 0 -> 1 -> 1 -> 1 (进位变化过程)
7 8 9 (反转后的 a)
+ 6 5 0 (反转后的 b)
-----------------
(ans) 3 -> 4 -> 0 (倒序的结果 ans)
```
* **i = 0 (个位):** `carry` 是 `0`。`7 + 6 + 0 = 13`。结果存 `3`,新 `carry` 为 `1`。`ans = "3"`。
* **i = 1 (十位):** `carry` 是 `1`。`8 + 5 + 1 = 14`。结果存 `4`,新 `carry` 为 `1`。`ans = "34"`。
* **i = 2 (百位):** `carry` 是 `1`。`9 + 0 + 1 = 10`。结果存 `0`,新 `carry` 为 `1`。`ans = "340"`。
**第四步:处理最终进位**
循环结束后,检查是否还有剩余的进位。在这里,`carry` 等于 `1`,需要追加到结果的末尾。
* `ans` 变为 `"3401"`。
**第五步:再次反转**
我们得到的 `ans` 是最终结果的倒序。现在,我们把它反转回来,得到正确的答案。
* `"3401"` 反转后得到 `"1043"`。
至此,计算完成!这个过程将一个复杂的、需要处理多种情况的双指针问题,转化成了一个简单的、逻辑统一的单循环问题。
#### **四:代码实现**
```cpp
string add(string a, string b) {
// 将两个输入字符串都反转
// 目的:让个位在 [0] 位置,方便从左到右处理
reverse(a.begin(), a.end());
reverse(b.begin(), a.end());
// 在较短的字符串末尾补 0,使其与较长的等长
// 目的:统一循环次数,避免复杂的边界判断
while (a.size() < b.size()) a += '0';
while (b.size() < a.size()) b += '0';
string ans = ""; // 存放倒序的结果
int carry = 0; // 初始化进位
// 从左到右逐位相加 (i=0 是个位,i=1 是十位...)
for (int i = 0; i < a.size(); i++) {
// 计算当前第 i 位的总和
int sum = (a[i] - '0') + (b[i] - '0') + carry;
// 将结果的个位数(字符形式)拼接到 ans 的末尾
ans += to_string(sum % 10);
// 更新进位,为下一轮做准备
carry = sum / 10;
}
// 如果循环结束后仍有进位 (如 99+1=100), 则追加到 ans 末尾
if (carry > 0) {
ans += to_string(carry);
}
// 将倒序的结果 ans 再次反转,得到最终答案
reverse(ans.begin(), ans.end());
return ans;
}
```
#### **第五部分:总结与要点**
* **核心思路**:通过 `反转 -> 补零 -> 相加 -> 再反转` 的流程,将问题简化。
* **设计思想**:**将复杂性从核心循环中剥离,移入预处理和后处理阶段**。
* 通过预处理将所有输入(无论长度如何)都统一为相同长度的字符串,避免了在核心循环中进行繁琐的长度判断,大大减少了出错的可能性。
* **技巧**:在**反转后**的字符串**末尾**补零,是实现**原始数字**在**开头**对齐的关键。
@@ -0,0 +1,185 @@
### **C++ 算法讲义:高精度除法**
#### **一、 引言**
##### **1.1 问题引入**
我们已经掌握了如何让计算机处理大整数的加、减、乘法。现在迎来了高精度运算中逻辑最复杂的一环:**高精度除法**。
思考一个场景:处理宏观经济数据时,计算一个国家千万亿级别的 GDP 除以数亿人口的人均产值;或是密码学中,对两个极长的大素数进行除法和取模运算。
例如:`1,000,000,000,000,000,000 ÷ 123,456,789 = ? ... ?`
C++ 内置的 `/` 和 `%` 运算符在面对这种规模的数字时会因溢出而失效。我们必须设计一种算法,教会计算机像人类一样进行“大数 ÷ 大数”的精确计算。
##### **1.2 价值阐述**
高精度除法是高精度运算系列中综合性最强的算法。因为它的实现不仅需要严密的逻辑,还深度依赖之前学过的高精度减法和比较算法。并且它是实现更高级数学算法(如大数求最大公约数、快速幂取模、大数开方等)的基础工具。
#### **二、 核心思想:模拟竖式除法**
为了让计算机“学会”除法,我们回顾人类是如何手工计算的。以 `859 ÷ 21` 为例:
```text
4 0 <-- 商 (Quotient)
______
21 ) 8 5 9 <-- 被除数 (Dividend)
8 4 <-- 21 * 4
---
1 9 <-- 85 - 84 = 1,然后落下 '9',组成新余数 19
0 0 <-- 19 不够除 21 (商 0)
---
1 9 <-- 最终余数 (Remainder)
```
**竖式除法的动作分解:**
1. **取位**:从被除数的最高位开始,依次向右提取一个“片段”。
2. **试商**:判断当前“片段”里最多包含几个除数。得出的数字即为商的一位。
3. **相减**:从当前片段中减去 `除数 × 商`。
4. **落位 **:将被除数的下一位“落”下,拼接到相减后的余数末尾,形成下一轮的新片段。
5. **循环往复**:重复上述过程,直到被除数的所有位都被处理完毕。
**如何让计算机进行“试商”?**
人类可以通过直觉快速估算出 `85` 里面有 `4` 个 `21`。但计算机需要精确的指令告诉它怎么做。
最直接的“试商”方法是**连减法**:
在一个内部循环中,不断地从 `当前片段` 中减去 `除数`,并用一个计数器记录减成功的次数。直到 `当前片段` 小于 `除数` 为止。**这个计数器的值,就是当前这一位的商。**
**算法核心逻辑总结:**
从被除数 `A` 的最高位起,逐位构建 `当前余数 R`。每一步,将 `A` 的新一位字符拼到 `R` 的末尾,然后用一个内部 `while` 循环,统计 `R` 能够减去多少次除数 `B`。减去的次数存入最终的商,减完后剩下的值作为下一轮的初始余数。
---
#### **三、 实例与图解演示**
**具体案例**:用算法逻辑推演 `859 ÷ 21`。
**前置要求**:我们假定已经拥有了两个辅助函数:`isGreaterOrEqual(A, B)`(判断大数A是否大于等于B)和 `sub(A, B)`(执行大数减法),其中 `R` 代表余数部分。
**分步演示表**:
| 步骤 | 落位 | 拼接前 `R` | 拼接后 `R` | 试商过程 (连减法寻找商 `d`) | 记录商 | 更新后的 `R` |
| :--- | :----------- | :--------- | :--------- | :--------------------------------------- | :------ | :----------- |
| 1 | `A[0] = '8'` | `""` | `"8"` | `while("8" >= "21")` 假,`d=0` | `"0"` | `"8"` |
| 2 | `A[1] = '5'` | `"8"` | `"85"` | `while("85" >= "21")` 真,共减4次,`d=4` | `"04"` | `"1"` |
| 3 | `A[2] = '9'` | `"1"` | `"19"` | `while("19" >= "21")` 假,`d=0` | `"040"` | `"19"` |
**最终结果**:
* 得到原始商 `"040"`,我们需要去除前导零,最终商为 `"40"`。
* 最终 `R` 的值为 `"19"`,这就是余数。
---
#### **四、 代码实现**
高精度除法它实现严重大数比较和减法。
```cpp
// 判断大数 a 是否大于或等于 b (a >= b 返回 true)
bool isGreaterOrEqual(string a, string b) {
if (a.length() != b.length()) {
return a.length() > b.length();
}
return a >= b; // 长度相同时,按字典序比较即可等效于数值比较
}
// 高精度减法 (前提是 a >= b,且均为正数)
string sub(string a, string b) {
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
while (b.size() < a.size()) b += '0';
string ans = "";
int borrow = 0;
for (int i = 0; i < a.size(); i++) {
int diff = (a[i] - '0') - (b[i] - '0') - borrow;
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
ans += to_string(diff);
}
// 去除减法结果末尾多余的 '0' (反转后就是前导零)
while (ans.size() > 1 && ans.back() == '0') {
ans.pop_back();
}
reverse(ans.begin(), ans.end());
return ans;
}
// 高精度除法
// 返回 pair<string, string>,分别代表 {商, 余数}
pair<string, string> divide(string a, string b) {
// 特判处理
if (b == "0") return {"除0错误", ""};
if (a == b) return {"1", "0"};
if (!isGreaterOrEqual(a, b)) return {"0", a}; // 被除数小于除数
string quotient = ""; // 用于累加商
string remainder = ""; // 用于维护当前片段的余数
// 模拟竖式除法:从最高位向低位遍历被除数 a
for (int i = 0; i < a.length(); i++) {
// 落位:将当前位拼接到余数末尾
remainder += a[i];
// 清理余数可能产生的前导零 (例如 05 -> 5)
if (remainder.size() > 1 && remainder[0] == '0') {
remainder.erase(remainder.begin());
}
// 试商 (连减法)
int count = 0;
// 只要当前余数大于等于除数,就继续减
while (isGreaterOrEqual(remainder, b)) {
remainder = sub(remainder, b); // 执行大数减法
count++;
}
// 记录本位计算出的商
quotient += to_string(count);
}
// 清理商的前导零 (例如 040 -> 40)
while (quotient.size() > 1 && quotient[0] == '0') {
quotient.erase(quotient.begin());
}
// 处理恰好整除的情况
if (remainder.empty()) remainder = "0";
return {quotient, remainder};
}
```
#### **五、 算法瓶颈与优化方向**
**思考:连减法试商的效率问题**
上述代码中,我们采用了“连减法”进行试商:`while (isGreaterOrEqual(remainder, b))`。
这是一种直观但效率不高的策略。最坏的情况下(例如某一位的商是 `9`),内部的 `while` 循环需要执行 `9` 次高精度减法。如果被除数极长,这将极大地拖慢程序运行速度。
**二分法试商**
为了提高试商效率,我们可以将其转化为一个**查找问题**。每一位的商必然是一个在 `0` 到 `9` 之间的整数。
我们可以利用**二分查找法**在这个小区间内快速定位商的值。
对于一个猜测的商 `mid`,我们只需计算 `B × mid`(大数乘以单精度数),然后将其与 `remainder` 比较:
* 如果 `B × mid > remainder`:说明猜测的商太大了,正确答案在左半区间 `[0, mid - 1]`。
* 如果 `B × mid <= remainder`:说明猜测的商可能是对的,但也可能还能更大,我们记录下当前的 `mid`,并继续在右半区间 `[mid, 9]` 尝试。
通过二分查找,每次试商的次数从最多 9 次降低到大约 3-4 次。这在处理超大规模数字时,可以带来显著的性能提升。
### 六. 总结
1. **核心思想**:高精度除法是对手动**竖式除法**的模拟。
2. **关键步骤**:“落位” -> “试商” -> “相减” -> “记录商” 的循环。
3. **实现难点**:“试商”过程。简单实现是**循环减法**,高效实现是**二分查找+乘法**。
4. **基础依赖**:高精度除法建立在**比较和高精度**减法之上。