当两个数大到连 long long 都装不下时,回到小学竖式的老办法:用数组存下每一位数字,一位一位地加、一位一位地进位。
C++ 里最大的整数类型 long long 差不多能装下 19 位十进制数字,平时够用了。但有些题目要求的数字远远超过这个范围——比如 100!(100 的阶乘)算出来有 158 位,这时候 long long 也会"装不下"。
| 1 | long long a = 9000000000000000000; // 已经接近 long long 的上限 |
| 2 | long long b = 9000000000000000000; |
| 3 | cout << a + b; // 期望是一个 19 位的大正数…… |
| 4 | // 实际输出:-446744073709551616(居然变成了负数!) |
long long 的空间是固定大小的,一旦真实结果超出了它能装的范围,多出来的部分就会丢失,程序不会报错,只会安安静静地给出一个错误答案,很容易被忽略(想搞清楚背后的原理,可以看 2.3 节"原码反码补码",这里先记住结论就够用)。既然固定大小的变量不够用,那就换个思路:不再指望"一个变量装下整个数",而是把数字拆成一位一位,像小学做竖式加法那样,一位一位地算。这就是高精度算法的核心想法。
回忆一下你小学怎么算 7 + 8 这种"个位相加超过 10"的情况:先算个位 7+8=15,这一位写 5,多出来的 1 进到十位上去。这个"进位"的动作,就是整个高精度算法唯一需要模拟的核心规则。
7 + 8 = 15 → 写 5,进 1,结果是 15要一位一位地算,第一步得先把数字"拆开"。输入的大数通常是字符串形式给出的(比如 "12345"),我们把它转换成一个数字数组,一个数组元素存一位。这里先约定一件事:数组下标 0 存个位,下标越大存的位越高——也就是"倒着存"。
| 1 | string s; |
| 2 | cin >> s; // 输入 "12345" |
| 3 | |
| 4 | int len = s.size(); |
| 5 | vector<int> a(len, 0); // 有几位数字,就开多长 |
| 6 | |
| 7 | for (int i = 0; i < len; i++) |
| 8 | { |
| 9 | a[i] = s[len - 1 - i] - '0'; // 从字符串末尾往前取,字符转数字见 2.2 节 |
| 10 | } |
vector<int> a(len, 0) 会按数字的实际长度自动开好数组,并且每个位置自动填 0,不用我们自己猜一个够大的固定长度、也不用手动清零。用法和普通数组几乎一样,写 a[i] 取第 i 位就行。为什么偏偏要"倒着存",而不是按平时读数的顺序正着存?加法的进位方向是从低位传向高位:个位进位会影响十位,十位进位会影响百位……如果让数组下标 0 存最高位(正着存),进位的时候反而要往下标更小的方向找位置,而且还要提前空出最高位可能因为进位多出一位的空间,写起来别扭。倒着存之后,下标只会一路递增,进位时直接往后面写就行,正好和 for 循环 i 递增的方向一致。
i++ 的方向相反,还得额外考虑往前插一位i++ 的方向一致,多一位就往数组后面加一格下面把前面拆开讲的每一步拼在一起,写成一个可以直接编译运行的完整程序。建议自己动手敲一遍、编译运行看看结果,比单纯读代码印象更深:
| 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 | int len = max(len1, len2); // 取较长的位数,数组按它来开 |
| 12 | |
| 13 | vector<int> a(len, 0), b(len, 0), c(len + 1, 0); // c 多开一位,留给最后可能的进位 |
| 14 | |
| 15 | for (int i = 0; i < len1; i++) // 把 s1 倒序拆进数组 a(下标0存个位) |
| 16 | { |
| 17 | a[i] = s1[len1 - 1 - i] - '0'; // 字符转数字,从字符串末尾往前取 |
| 18 | } |
| 19 | for (int i = 0; i < len2; i++) // s2 同样倒序拆进数组 b |
| 20 | { |
| 21 | b[i] = s2[len2 - 1 - i] - '0'; |
| 22 | } |
| 23 | |
| 24 | int carry = 0; // 进位,一开始没有进位 |
| 25 | for (int i = 0; i < len; i++) // 从个位(下标0)开始逐位相加 |
| 26 | { |
| 27 | int sum = a[i] + b[i] + carry; // 本位 + 本位 + 上一位的进位 |
| 28 | c[i] = sum % 10; // 个位数留下 |
| 29 | carry = sum / 10; // 十位数往前进 |
| 30 | } |
| 31 | |
| 32 | if (carry) // 最后还剩一个进位,说明结果多了一位 |
| 33 | { |
| 34 | c[len] = carry; // 多出来的这一位单独存进最高位 |
| 35 | len++; // 结果的总位数也要跟着加1 |
| 36 | } |
| 37 | |
| 38 | for (int i = len - 1; i >= 0; i--) // 倒着输出,从高位到低位 |
| 39 | { |
| 40 | cout << c[i]; |
| 41 | } |
| 42 | cout << endl; |
| 43 | |
| 44 | return 0; |
| 45 | } |
4827 358,再试试这几组输入,检验自己是否真的理解了每一步:999 1(结果多一位,1000)、0 0(结果应该是 0,不是空白)、两个都是 20 位以上的大数(验证 long long 装不下也能算对)。如果输出和预期不一致,回到上面的追踪表格,对照自己的代码一步步排查是哪一步出了偏差。上面完整代码里第 25~30 行的循环,就是下面要详细拆解的逐位相加过程;第 32~36 行处理的是循环结束后可能剩下的最后一次进位。数组存好之后,加法就是模拟小学竖式:从个位(下标 0)开始,每一位对应相加,满 10 就往高一位进 1,一直算到两个数都用完为止。
9+5=14,写 4 进 1;十位 8+1+1(进位)=10,写 0 进 1;百位 4+0+1(进位)=5,写 5。三步拼起来就是 504,和 489+15=504 完全对得上。489 + 5,位数较短的那个数,多出来的高位就当成 0 参与运算——代码里只要让循环跑到"两个数中较长的那个"的位数,较短的数组越界的部分自然就是 0(因为数组一开始就整体初始化成了 0),不需要额外写判断。为了更直观地看清楚"程序到底是怎么一步步算出来的",下面用一个稍微大一点的例子——4827 + 358——把循环每一轮的状态都列出来,逐行对照着看:
| i | a[i] | b[i] | 进位(carry) | 本位和(sum) | c[i] = sum % 10 | 新进位 = sum / 10 |
|---|---|---|---|---|---|---|
| 0(个位) | 7 | 8 | 0 | 7+8+0=15 | 5 | 1 |
| 1(十位) | 2 | 5 | 1 | 2+5+1=8 | 8 | 0 |
| 2(百位) | 8 | 3 | 0 | 8+3+0=11 | 1 | 1 |
| 3(千位) | 4 | 0(b 越界补 0) | 1 | 4+0+1=5 | 5 | 0 |
循环结束后 carry = 0,不需要再多开一位。把 c 数组从高位(下标 3)往低位(下标 0)读出来,就是 5 1 8 5,也就是 5185——正好等于 4827 + 358。注意第 3 行(千位)里 b[3] 越界了,直接当成 0 处理,这正好对应上面"位数不同"的情况:358 只有 3 位,数组按较长的 4827(4 位)分配大小,多出来的 b[3] 本来就是初始化时留下的 0。
把本节内容汇总成几条最容易踩坑的规则:
99+1=100),循环结束后如果 carry 还不是 0,必须再多存一位,否则结果会少一位数字。c 必须开到 len+1,而不是 len——两数相加的结果最多比原来两数中较长的那个多出一位。如果 c 只开了 len 大小,c[len] = carry 这一行就会访问到数组之外的位置,需要格外小心。len-1 递减到 0,而不是从 0 递增——这是最容易顺手写反的地方。