乘法不再是简单的逐位对齐相加,而是要弄清楚"两个数各自哪一位相乘,结果该落到哪一个位置",这是本节的核心。
回忆一下小学怎么算 23 × 45 这种两位数乘两位数:先用 45 的个位 5 去乘 23,得到 115;再用 45 的十位 4 去乘 23,得到 92,但这次要把结果整体往左错开一位(变成 920);最后把两次结果相加,115 + 920 = 1035。这个"错位"的动作,正是本节要在代码里讲清楚的核心。
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 位。
| 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 |
a=[3,2](a[0]=个位3,a[1]=十位2),45 倒序存储是 b=[5,4]。四对数字两两相乘,乘积按"下标相加"的规则分别落到结果的第 0、1、1、2 位——上方两个橙色标记的乘积(12 和 10)都落在了位置 1,说明同一个位置可能会收到好几份贡献,需要全部累加起来。| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | using namespace std; |
| 4 | |
| 5 | int 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 | } |
O(len1 × len2),比加减法的 O(len) 高出一个数量级。如果两个数都有几千位,这种"暴力枚举每一对下标"的写法可能会超时,需要更高级的算法(如 FFT 快速傅里叶变换)来优化,不过那已经超出了本节的范围。999 999(连续进位)、100 100(结果末尾有很多 0)、两个 10 位以上的大数相乘(验证 long long 也算不动的场景)。上面完整代码里第 31~35 行专门处理进位,这一节把这段逻辑单独拎出来,一步步讲清楚它到底在做什么。
把上面 4 个乘积按照落到的位置分组:位置 0 只收到 1 个乘积(15);位置 1 收到了 2 个乘积(12 和 10);位置 2 只收到 1 个乘积(8)。分组累加之后,得到的是一个"还没规整"的中间结果——比如位置 1 是 12+10=22,这个 22 本身已经不是一个"位"上该有的数字了(一位数字只能是 0~9),不能直接当成十位上的数字用。
这时候需要做的事情,和加法的进位处理完全一样:从最低位(位置 0)开始,往高位方向扫一遍,把每一位超过 9 的部分拆出来,作为"进位"传给更高的一位,让每一位重新变回 0~9 的范围。唯一的区别是:加法进位之前只有两个数相加,乘法进位之前,同一个位置可能已经堆了好几个乘积的和。
%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) |
|---|---|---|---|---|
| 0 | 15 | 15 + 0 = 15 | 5 | 1 |
| 1 | 22 | 22 + 1 = 23 | 3 | 2 |
| 2 | 8 | 8 + 2 = 10 | 0 | 1 |
| 3(原本是 0) | 0 | 0 + 1 = 1 | 1 | 0 |
从位置 3 读到位置 0,就是 1 0 3 5,也就是 1035。注意最后位置 3 本来是空的(初始值 0),但因为位置 2 产生了新的进位,还是被"激活"用上了——这也是为什么结果 vector 一开始要多开出 len1 + len2 位空间,即使乘积本身没有直接落到某个高位,进位也可能会填进去。
实际做题时,更常见的场景是"一个高精度大数,乘以一个 long long 范围内的普通整数"(比如求阶乘 n!,本质就是把一个不断变大的高精度数反复乘以一个小整数)。这种情况不需要双层循环,思路和加法几乎一样简单:
| 1 | // 计算 20! (20 的阶乘),结果远超 long long 范围 |
| 2 | vector<int> c(1, 1); // 初始值为 1(只有一位,个位是 1) |
| 3 | |
| 4 | for (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 | |
| 20 | for (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) 循环把进位完全展开成多位再退出,不能只处理一次就了事。