Initial commit: C++ courseware
This commit is contained in:
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. 在性能上,**竖式模拟法**优于分解相加法。
|
||||
Reference in new issue
Block a user