8.3 KiB
8.3 KiB
C++算法讲义:高精度乘法
第一部分:大数 × 小数
在学习复杂的“大数 × 大数”之前,我们先解决一个简化版的问题:一个很大的数乘以一个小数(int 范围内)。这是我们后续大数 × 大数方法一的基础。
核心思想 (大数 × 小数)
这个过程和高精度加法极其相似,都是逐位处理。不同之处在于,我们不再是 A[i] + B[i],而是用大数的每一位 A[i] 去乘以那个小数 b,并且进位不再是 0 或 1,可以是任意大小的数字。
算法步骤:
- 将大数
A(字符串)倒序,方便从个位开始处理。 - 设置一个进位
carry = 0。 - 遍历倒序后的大数
A的每一位A[i]。 - 计算
(A[i] - '0') * b + carry,得到当前位的乘积结果res。 - 新结果的当前位是
res % 10。 - 新的进位是
res / 10。 - 循环结束后,如果
carry还有剩余,需要继续处理完。 - 最后将得到的结果字符串再次反转,即为最终答案。
代码实现 (大数 × 小数)
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
我们把一个“大数 × 大数”问题,巧妙地转化成了:
- 一个“大数 × 小数” (
321 × 5) - 另一个“大数 × 小数” (
321 × 4),再在其结果末尾补0 - 最后把这两个结果用“大数 + 大数”加起来!
这个思路的精髓在于复用。我们只需要实现 mul(string, int) 和 add(string, string) 这两个函数,就可以组合出 mul(string, string) 的功能。
代码实现 (分解相加法)
// 注意:此函数依赖我们之前已经实现好的高精度加法函数, 以及我们上面刚实现的 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 位上!
优化策略:为了避免复杂的实时进位,我们采用先累加,后统一进位的策略。
- 创建一个足够长的结果数组
res(长度为n+m)。 - 用两层循环,计算所有
A[i] × B[j],并直接累加到res[i+j]上,此时res里可能存着大于9的数。 - 最后,从头到尾遍历
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. 代码实现 (竖式模拟法)
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;
}
方法对比
结论:在算法竞赛中,方法二(竖式模拟法)是主要的选择。方法一则胜在思路巧妙,体现了优秀的模块化和问题分解思想。
总结
- 高精度乘法有两种主流思路:分解相加和竖式模拟。
- 分解相加:将
大数×大数转化为多次大数×小数和大数+大数,核心是复用。 - 竖式模拟:用
vector存储中间结果,核心是C[i+j] += A[i] × B[j]公式和先累加后进位的思想。 - 在性能上,竖式模拟法优于分解相加法。