← 目录 / 算法文档 · 模块一 数学基础 / 1.11 高精度乘法

1.11 高精度乘法

乘法不再是简单的逐位对齐相加,而是要弄清楚"两个数各自哪一位相乘,结果该落到哪一个位置",这是本节的核心。

本页目录
① 核心思路:位置对应相乘

回忆一下小学怎么算 23 × 45 这种两位数乘两位数:先用 45 的个位 5 去乘 23,得到 115;再用 45 的十位 4 去乘 23,得到 92,但这次要把结果整体往左错开一位(变成 920);最后把两次结果相加,115 + 920 = 1035。这个"错位"的动作,正是本节要在代码里讲清楚的核心。

竖式乘法:23 × 45 = 1035(第二行"错位"往左移一位)
1
·
·
1
千位
0
1
9
0
百位
0
1
2
3
十位
0
5
0
5
个位
第一行是 23 × 5 = 115,按千百十个对齐是 ·115;第二行是 23 × 4 = 92,因为乘的是十位(相当于再乘以 10),整体往左错开一位,对齐后变成 920·。两行逐列相加:个位 5+0=5;十位 1+2=3;百位 1+9=10,写 0 进 1;千位 0+0+1(进位)=1。合起来正好是 1035

加减法都是"同一位置对齐运算"——个位对个位、十位对十位,两个数在同一个位置上的数字直接发生关系。但乘法完全不同:数组下标为 i 的位和下标为 j 的位相乘,结果不是放在第 i 位或第 j 位,而是要放到下标 i + j 的位置——这正好对应上面竖式里"往左错开一位"的动作,只是竖式把错位画在纸上,代码里用下标的加法来实现。

这背后的道理和小学数学是一致的:数组下标 i 存的并不是"数字本身",而是"第 i 位代表 10 的 i 次方"这个位权。比如下标 0 是个位,权重是 10⁰=1;下标 1 是十位,权重是 10¹=10。两个位权相乘,指数是相加的(10ⁱ × 10ʲ = 10^(i+j)),所以乘积自然就落在了第 i+j 位。

23 × 45 中,每一位相乘落在哪个位置(颜色 = 目标位置)
b[0]=5 b[1]=4
a[0]=3 15
位置 0+0=0
12
位置 0+1=1
a[1]=2 10
位置 1+0=1
8
位置 1+1=2
23 倒序存储是 a=[3,2](a[0]=个位3,a[1]=十位2),45 倒序存储是 b=[5,4]。四对数字两两相乘,乘积按"下标相加"的规则分别落到结果的第 0、1、1、2 位——上方两个橙色标记的乘积(12 和 10)都落在了位置 1,说明同一个位置可能会收到好几份贡献,需要全部累加起来。
② 完整代码:高精度 × 高精度
C++ · 高精度乘法(完整可运行程序)
1#include <iostream>
2#include <vector>
3using namespace std;
4
5int main()
6{
7 string s1, s2;
8 cin >> s1 >> s2; // 用字符串读入两个大数
9
10 int len1 = s1.size(), len2 = s2.size();
11 vector<int> a(len1, 0), b(len2, 0); // a、b 各自按自己的位数分配空间
12
13 for (int i = 0; i < len1; i++)
14 {
15 a[i] = s1[len1 - 1 - i] - '0'; // 倒序存储,a[0] 是个位
16 }
17 for (int i = 0; i < len2; i++)
18 {
19 b[i] = s2[len2 - 1 - i] - '0'; // b 同样倒序存储
20 }
21
22 vector<int> c(len1 + len2, 0); // 结果最多 len1+len2 位,先按最大可能开够空间
23 for (int i = 0; i < len1; i++)
24 {
25 for (int j = 0; j < len2; j++)
26 {
27 c[i + j] += a[i] * b[j]; // 核心:a[i]、b[j] 的乘积累加到第 i+j 位
28 }
29 }
30
31 for (int i = 0; i < (int)c.size() - 1; i++) // 统一处理进位:从低位到高位扫一遍
32 {
33 c[i + 1] += c[i] / 10; // 本位超出 9 的部分进给高一位
34 c[i] %= 10; // 本位只留个位数字
35 }
36
37 int len = (int)c.size();
38 while (len > 1 && c[len - 1] == 0) // 去掉结果高位多余的 0
39 {
40 len--;
41 }
42
43 for (int i = len - 1; i >= 0; i--) // 从最高位往低位输出
44 {
45 cout << c[i];
46 }
47 cout << endl;
48
49 return 0;
50}
运行示例
输入:23 45
输出:1035
⚠️
时间复杂度:双层循环让高精度乘法的时间复杂度是 O(len1 × len2),比加减法的 O(len) 高出一个数量级。如果两个数都有几千位,这种"暴力枚举每一对下标"的写法可能会超时,需要更高级的算法(如 FFT 快速傅里叶变换)来优化,不过那已经超出了本节的范围。
✏️
动手试一试:试试这几组输入,检验自己是否真的理解了每一步:999 999(连续进位)、100 100(结果末尾有很多 0)、两个 10 位以上的大数相乘(验证 long long 也算不动的场景)。
③ 统一处理进位

上面完整代码里第 31~35 行专门处理进位,这一节把这段逻辑单独拎出来,一步步讲清楚它到底在做什么。

把上面 4 个乘积按照落到的位置分组:位置 0 只收到 1 个乘积(15);位置 1 收到了 2 个乘积(1210);位置 2 只收到 1 个乘积(8)。分组累加之后,得到的是一个"还没规整"的中间结果——比如位置 1 是 12+10=22,这个 22 本身已经不是一个"位"上该有的数字了(一位数字只能是 0~9),不能直接当成十位上的数字用。

这时候需要做的事情,和加法的进位处理完全一样:从最低位(位置 0)开始,往高位方向扫一遍,把每一位超过 9 的部分拆出来,作为"进位"传给更高的一位,让每一位重新变回 0~9 的范围。唯一的区别是:加法进位之前只有两个数相加,乘法进位之前,同一个位置可能已经堆了好几个乘积的和。

按位置累加乘积,再统一处理进位(颜色对应上图的位置)
1
0
1
位置 3
2
8
0
位置 2
1
22
3
位置 1
0
15
5
位置 0
每一列从上到下:本位收到的进位(顶部数字)→ 这一位原始的累加值(中间较大的数字)→ 加起来之后的最终结果(底部绿色数字,即 %10 的余数),多出来的部分作为新的进位,传给左边一列。位置 0 没有进位输入,原始值 15 直接留 5,进 1;位置 1 原始值 22 加上进位 1 变成 23,留 3,进 2;位置 2 原始值 8 加上进位 2 变成 10,留 0,进 1;位置 3 本来一个乘积都没有(原始值是 0),但收到了位置 2 传来的进位 1,直接变成 1,成了结果最高位。从左到右读出 1 0 3 5,正好是 23 × 45 = 1035

把上面这段话拆成表格,对照代码里 c[i+1] += c[i] / 10; c[i] %= 10; 这两行逐位扫描的过程,会更清楚每一步具体在做什么:

位置原始累加值+ 来自低位的进位本位最终数字(%10)产生的新进位(/10)
01515 + 0 = 1551
12222 + 1 = 2332
288 + 2 = 1001
3(原本是 0)00 + 1 = 110

从位置 3 读到位置 0,就是 1 0 3 5,也就是 1035。注意最后位置 3 本来是空的(初始值 0),但因为位置 2 产生了新的进位,还是被"激活"用上了——这也是为什么结果 vector 一开始要多开出 len1 + len2 位空间,即使乘积本身没有直接落到某个高位,进位也可能会填进去。

📖
和加减法的进位有什么不同?加法的进位是"边算边处理"——算完一位立刻处理进位再算下一位;乘法则是先把所有位置该收到的乘积都累加完,最后再统一从低位到高位扫一遍处理进位。这是因为乘法某个位置可能同时收到好几个乘积的贡献(如上面的位置 1),提前进位反而会打乱后续乘积的累加,所以要等全部乘完、加完之后再统一处理。
④ 简化版:高精度 × 低精度

实际做题时,更常见的场景是"一个高精度大数,乘以一个 long long 范围内的普通整数"(比如求阶乘 n!,本质就是把一个不断变大的高精度数反复乘以一个小整数)。这种情况不需要双层循环,思路和加法几乎一样简单:

C++ · 高精度 × 低精度(例如阶乘)
1// 计算 20! (20 的阶乘),结果远超 long long 范围
2vector<int> c(1, 1); // 初始值为 1(只有一位,个位是 1)
3
4for (int k = 1; k <= 20; k++) // 依次乘以 1, 2, 3, ..., 20
5{
6 int carry = 0; // 这一轮乘法的进位
7 for (int i = 0; i < (int)c.size(); i++)
8 {
9 int cur = c[i] * k + carry; // 本位数字直接乘以 k,再加上进位
10 c[i] = cur % 10;
11 carry = cur / 10;
12 }
13 while (carry) // 进位可能不止一位(k 本身可以很大),要循环处理完
14 {
15 c.push_back(carry % 10);
16 carry /= 10;
17 }
18}
19
20for (int i = (int)c.size() - 1; i >= 0; i--)
21{
22 cout << c[i];
23}
💡
为什么这个版本更简单?因为 k 是一个普通整数,不需要拆成数组,只要用高精度数组的每一位分别乘以这个整数、加上进位即可——不需要"位置相加"的双层循环,也不用等到最后才统一处理进位(这里的进位是"边乘边处理",和加法的进位方式一致)。这个写法在求阶乘、求 2 的 n 次方等题目中非常常用。
⑤ 常见陷阱

把本节内容汇总成几条最容易踩坑的规则:

结果数组开小了:两个数相乘,结果的位数最多是 len1 + len2 位(不会更多),但也不会自动变小,声明结果 vector 时必须留出这么多空间,否则 c[i+j] 会访问越界。
过早处理进位:如果在双层循环内部就尝试处理进位,会因为同一位置后续还会收到新的乘积贡献而把已经进位的结果搅乱。正确做法是先把所有乘积按位置累加完毕,再统一从低位到高位扫一遍处理进位。
高精度乘低精度时忘记处理连续进位:低精度乘数 k 如果比较大(比如 k=1000),单次的 carry 可能不止一位数字,简化版代码里必须用 while(carry) 循环把进位完全展开成多位再退出,不能只处理一次就了事。
🏆
接下来:高精度除法(1.12 节)同样有"高精度 ÷ 高精度"和"高精度 ÷ 低精度"两个版本,其中低精度版本更常用、也更简单——模拟竖式除法,从最高位开始,带着余数往下走。