乘法不再是简单的逐位对齐相加,而是要弄清楚"两个数各自哪一位相乘,结果该落到哪一个位置",这是本节的核心。
加减法都是"同一位置对齐运算"——个位对个位、十位对十位。但乘法完全不同:数组下标为 i 的位和下标为 j 的位相乘,结果不是放在第 i 位或第 j 位,而是要放到下标 i + j 的位置。
这背后的道理其实和小学数学是一致的:数组下标 i 存的是"10 的 i 次方"这一位,两个"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,说明同一个位置可能会收到好几份贡献,需要全部累加起来。把上面 4 个乘积按位置分组累加,会得到一个"每一位还没规整"的中间结果——某些位置的数字可能远大于 9(因为多个乘积叠在了一起),需要像加法一样再做一次进位处理,把每一位规整回 0~9 的范围。
15,留个位 5,进 1 到位置 1;位置 1 原本是 22,加上进位 1 变成 23,留 3,进 2 到位置 2;位置 2 原本是 8,加上进位 2 变成 10,留 0,再进 1 到位置 3。最终从高到低读出 1 0 3 5,正好是 23 × 45 = 1035。| 1 | string s1, s2; |
| 2 | cin >> s1 >> s2; |
| 3 | |
| 4 | int len1 = s1.size(), len2 = s2.size(); |
| 5 | vector<int> a(len1, 0), b(len2, 0); |
| 6 | |
| 7 | for (int i = 0; i < len1; i++) |
| 8 | { |
| 9 | a[i] = s1[len1 - 1 - i] - '0'; |
| 10 | } |
| 11 | for (int i = 0; i < len2; i++) |
| 12 | { |
| 13 | b[i] = s2[len2 - 1 - i] - '0'; |
| 14 | } |
| 15 | |
| 16 | vector<int> c(len1 + len2, 0); // 结果最多 len1+len2 位 |
| 17 | for (int i = 0; i < len1; i++) |
| 18 | { |
| 19 | for (int j = 0; j < len2; j++) |
| 20 | { |
| 21 | c[i + j] += a[i] * b[j]; // 核心:乘积累加到第 i+j 位 |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | // 统一处理进位:从低位到高位扫一遍 |
| 26 | for (int i = 0; i < (int)c.size() - 1; i++) |
| 27 | { |
| 28 | c[i + 1] += c[i] / 10; // 本位超出 9 的部分进给高一位 |
| 29 | c[i] %= 10; // 本位只留个位数字 |
| 30 | } |
| 31 | |
| 32 | // 去掉结果高位多余的 0 |
| 33 | int len = (int)c.size(); |
| 34 | while (len > 1 && c[len - 1] == 0) |
| 35 | { |
| 36 | len--; |
| 37 | } |
| 38 | |
| 39 | for (int i = len - 1; i >= 0; i--) |
| 40 | { |
| 41 | cout << c[i]; |
| 42 | } |
O(len1 × len2),比加减法的 O(len) 高出一个数量级。如果两个数都有几千位,这种"暴力枚举每一对下标"的写法可能会超时,需要更高级的算法(如 FFT 快速傅里叶变换)来优化,不过那已经超出了本节的范围。实际做题时,更常见的场景是"一个高精度大数,乘以一个 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) 循环把进位完全展开成多位再退出,不能只处理一次就了事。