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

1.9 高精度加法

当两个数大到连 long long 都装不下时,回到小学竖式的老办法:用数组存下每一位数字,一位一位地加、一位一位地进位。

本页目录
① 核心思路:数组存储与逐位进位

C++ 里最大的整数类型 long long 差不多能装下 19 位十进制数字,平时够用了。但有些题目要求的数字远远超过这个范围——比如 100!(100 的阶乘)算出来有 158 位,这时候 long long 也会"装不下"。

C++ · 眼见为实:long long 也会"装不下"
1long long a = 9000000000000000000; // 已经接近 long long 的上限
2long long b = 9000000000000000000;
3cout << a + b; // 期望是一个 19 位的大正数……
4// 实际输出:-446744073709551616(居然变成了负数!)
💥
两个正数相加,结果是负数?这种情况叫溢出long long 的空间是固定大小的,一旦真实结果超出了它能装的范围,多出来的部分就会丢失,程序不会报错,只会安安静静地给出一个错误答案,很容易被忽略(想搞清楚背后的原理,可以看 2.3 节"原码反码补码",这里先记住结论就够用)。

既然固定大小的变量不够用,那就换个思路:不再指望"一个变量装下整个数",而是把数字拆成一位一位,像小学做竖式加法那样,一位一位地算。这就是高精度算法的核心想法。

回忆一下你小学怎么算 7 + 8 这种"个位相加超过 10"的情况:先算个位 7+8=15,这一位写 5,多出来的 1 进到十位上去。这个"进位"的动作,就是整个高精度算法唯一需要模拟的核心规则。

热身:个位相加超过 10,怎么进位
1
·
·
1
十位
0
7
8
5
个位
7 + 8 = 15 → 写 5,进 1,结果是 15
高精度算法要做的,就是把这个"逐位相加、满 10 进 1"的动作,用循环在数组上重复很多很多次——不管数字有 5 位还是 500 位,规则都完全一样。

要一位一位地算,第一步得先把数字"拆开"。输入的大数通常是字符串形式给出的(比如 "12345"),我们把它转换成一个数字数组,一个数组元素存一位。这里先约定一件事:数组下标 0 存个位,下标越大存的位越高——也就是"倒着存"。

字符串 "12345" 拆成数字数组(下标 0 存个位)
5
[0]
个位
4
[1]
十位
3
[2]
百位
2
[3]
千位
1
[4]
万位
原来的字符串从左到右是"万千百十个",拆进数组之后顺序反过来,下标 0 对应最后一个字符(个位),下标 4 对应第一个字符(万位)。
C++ · 字符串转数字数组(倒序存储)
1string s;
2cin >> s; // 输入 "12345"
3
4int len = s.size();
5vector<int> a(len, 0); // 有几位数字,就开多长
6
7for (int i = 0; i < len; i++)
8{
9 a[i] = s[len - 1 - i] - '0'; // 从字符串末尾往前取,字符转数字见 2.2 节
10}
🧰
这里为什么用 vector?vector<int> a(len, 0) 会按数字的实际长度自动开好数组,并且每个位置自动填 0,不用我们自己猜一个够大的固定长度、也不用手动清零。用法和普通数组几乎一样,写 a[i] 取第 i 位就行。

为什么偏偏要"倒着存",而不是按平时读数的顺序正着存?加法的进位方向是从低位传向高位:个位进位会影响十位,十位进位会影响百位……如果让数组下标 0 存最高位(正着存),进位的时候反而要往下标更小的方向找位置,而且还要提前空出最高位可能因为进位多出一位的空间,写起来别扭。倒着存之后,下标只会一路递增,进位时直接往后面写就行,正好和 for 循环 i 递增的方向一致。

❌ 正着存(下标 0 存最高位)
1[0]
2[1]
3[2]
4[3]
进位要往下标变小的方向传递,和循环 i++ 的方向相反,还得额外考虑往前插一位
✅ 倒着存(下标 0 存个位)
4[0]
3[1]
2[2]
1[3]
进位往下标变大的方向传递,和循环 i++ 的方向一致,多一位就往数组后面加一格
💡
只需要记住一句话:倒着存,是为了让"进位的方向"和"循环遍历的方向"保持一致,代码才能写得顺手。后面减法、乘法、除法都沿用这个约定。
② 完整代码实现

下面把前面拆开讲的每一步拼在一起,写成一个可以直接编译运行的完整程序。建议自己动手敲一遍、编译运行看看结果,比单纯读代码印象更深:

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 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
输出:5185
✏️
动手试一试:把上面的程序敲进编译器,除了 4827 358,再试试这几组输入,检验自己是否真的理解了每一步:999 1(结果多一位,1000)、0 0(结果应该是 0,不是空白)、两个都是 20 位以上的大数(验证 long long 装不下也能算对)。如果输出和预期不一致,回到上面的追踪表格,对照自己的代码一步步排查是哪一步出了偏差。
③ 模拟竖式:逐位相加与进位

上面完整代码里第 25~30 行的循环,就是下面要详细拆解的逐位相加过程;第 32~36 行处理的是循环结束后可能剩下的最后一次进位。数组存好之后,加法就是模拟小学竖式:从个位(下标 0)开始,每一位对应相加,满 10 就往高一位进 1,一直算到两个数都用完为止。

竖式演示:4 8 9 + 1 5 = 5 0 4
1
4
0
5
百位
1
8
1
0
十位
0
9
5
4
个位
从右往左(个位→十位→百位)逐位相加:个位 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——把循环每一轮的状态都列出来,逐行对照着看:

ia[i]b[i]进位(carry)本位和(sum)c[i] = sum % 10新进位 = sum / 10
0(个位)7807+8+0=1551
1(十位)2512+5+1=880
2(百位)8308+3+0=1111
3(千位)40(b 越界补 0)14+0+1=550

循环结束后 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 这一行就会访问到数组之外的位置,需要格外小心。
输出顺序搞反:数组是倒着存的(下标 0 是个位),但输出应该是从高位到低位(符合正常读数顺序),所以输出循环要从 len-1 递减到 0,而不是从 0 递增——这是最容易顺手写反的地方。
🏆
接下来:高精度减法(1.10 节)的整体框架和加法很像,但多了"借位"和"判断两数大小"两个新问题;高精度乘法(1.11 节)和除法(1.12 节)则分别对应竖式乘法和竖式除法,思路都是"先模拟小学算术,再用数组和循环实现"。