刷过《信息奥赛一本通》的同学,大概率会在第1223题的位置看到这个标题:An easy problem。翻译过来就是“一个简单的问题”,不少人第一眼就笑了,结果样例一交,WA,笑容消失。这道题出在高精度计算章节,名字带了点反讽味道:它确实不难,但专门收拾那些数组、字符串基本功不牢的人。今天我把这道题的拆解思路、完整实现和实战踩坑记录一次写清楚,给正在刷《一本通》或者备考算法基础的同学做一份能直接抄作业的参考。
1. 先看这道题:叫easy,到底easy在哪
1.1 数据范围暴露的真相
高精度计算的题目有个共同特征:数据范围写得很大,大到内置类型装不下。拿常见的版本来说,题目会给出两个很大的整数,可能是100位、200位甚至更长,让你求它们的和或者某种简单运算。
为什么这么大的数不能用常规方式处理?看一眼C++内置类型就知道:
int能表示的最大值大约是 21亿多,大概10位;long long能表示到 19 位左右;unsigned long long也就 20 位出头。
当数字达到几十位、上百位的时候,任何一种原生整数类型都无能为力。就算你硬着头皮用__int128,也就 38 位左右,照样撑不住。所以“大数”必须换一种存储思路:用字符串读进来,再用数组逐位存储,最后模拟手工竖式计算。
这个思路就是整个高精度运算的核心。
1.2 这道题考的三个基本功
很多初学者以为《一本通》1223考的是数学,其实它考的是三个计算机基础功:
- 字符串处理能力:大数以字符串形式输入,你得知道怎么把字符
'9'转成数字9,怎么反转字符串,怎么计算长度。 - 数组模拟能力:竖式加法需要把数字从低位到高位依次存进数组,然后按位相加、处理进位。数组下标、循环边界稍微搞错一个就全盘皆输。
- 代码严谨性:开头多一个前导零、最高位进位没存、数组越界访问,都会导致答案错误。这类问题在本地样例里很难暴露,一提交就暴露无遗。
从这个角度说,“easy”指的是思路简单,但“不简单”指的是边界和细节多。谁能一遍写对,谁的基本功就真到位了。
2. 高精度加法的核心细节:从字符串到数组
2.1 为什么不能直接算,非要数组
想象你面前有两个行李箱,标称容量是20位数字,现在让你装进一个100位的数字,怎么办?只能把数字拆成一串“数字卡片”,每张卡片上写一位,然后用某种方式把这些卡片摞起来管理。这就是字符串加数组的基本思想。
具体的做法是:
- 用
string或char[]接收输入的两个大数; - 把每一个字符转换成对应的数字,存入整数数组;
- 存储时倒序存放,也就是数字的最低位放在数组下标0的位置;
- 从下标0开始逐位相加,模拟竖式加法;
- 处理进位;
- 最后把结果数组倒着输出。
为什么一定要倒序存放?因为竖式加法是从低位加到高位,而字符串读取的时候高位在前、低位在后。如果正着存,那么数组下标越大代表位数越低,循环处理进位时,需要在数组尾部追加新位,很别扭。倒序存放之后,下标递增的方向正好就是位数递增的方向,进位只需要往下标更大的位置加就行,自然且高效。
2.2 ASCII那点事:'0'和0差多少
初学高精度的人,十个里有八个在字符转数字这里栽跟头。
char本质上存储的是ASCII码。字符'0'的ASCII码是48,'1'是49,以此类推,'9'是57。也就是说,字符'9'到数字9之间差了整整48。
所以把字符转数字要这样写:
int num = str[i] - '0';反过来,把数字转字符时加上'0'即可:
char ch = digit + '0';这里有两个常见错误:
- 直接把字符当成数字用,比如
int x = '1',赋值结果是49而不是1; - 记不牢ASCII码,写成
str[i] - 48。这样虽然也能运行,但可读性差,遇到不同环境容易犯迷糊。更推荐用'0'这种字符常量来相减,语义清晰,也符合C++规范。
2.3 数组开多大才算够
数组大小是个老生常谈却常被忽略的问题。题目说数字不超过100位,有的同学就开a[100]、b[100]的数组,结果两个100位数相加,最高位可能产生进位,结果长度变成101位。如果存完结果还要往高位进位,就很容易越界。
更稳妥的做法是:读入字符串后测量实际长度,运算时预留至少1位进位空间,数组整体再多开20个左右的位置。
比如这样:
const int MAXN = 205; int a[MAXN], b[MAXN], c[MAXN]; char sa[MAXN], sb[MAXN];假设题目最大位数是100,205就留出了充分的余量。即使两个100位数相加产生101位结果,数组也完全装得下。这个习惯不仅适用于加法,做乘法时更需要大数组,乘法结果长度是两数长度之和,空间需求更大。
3. 完整实现:两版代码逐行拆解
3.1 string版代码:更推荐新手先学
用C++的string类型实现高精度加法,是几个版本里最简洁、最不容易出错的写法:
#include <iostream> #include <string> #include <algorithm> using namespace std; string add(string a, string b) { int i = a.size() - 1; int j = b.size() - 1; int carry = 0; string res = ""; // 只要还有数位没有处理完,或者还有进位,就继续循环 while (i >= 0 || j >= 0 || carry) { int sum = carry; // 先带上一位的进位 if (i >= 0) sum += a[i] - '0'; // 当前位来自a if (j >= 0) sum += b[j] - '0'; // 当前位来自b carry = sum / 10; // 计算新的进位 res.push_back(sum % 10 + '0'); // 只保留个位 i--; j--; } // 上面推出来的顺序是低位到高位,需要反转才是正常数字顺序 reverse(res.begin(), res.end()); return res; } int main() { string a, b; while (cin >> a >> b) { cout << add(a, b) << endl; } return 0; }这段代码的核心逻辑集中在while循环里。循环条件写成i >= 0 || j >= 0 || carry,把“两个数都处理完但进位还没处理完”的情况也覆盖进去了。这个处理非常关键,否则两个最大的数相加,最后一位进位会丢掉。
carry = sum / 10计算进位,sum % 10保留个位,这两个运算是竖式加法的数学本质。哪怕你忘了进位规则,写几组数据推一遍也能捡起来。
3.2 字符数组版代码:竞赛中最常见的写法
string版虽然简洁,但不少竞赛选手习惯用C风格的char[],因为输入输出更接近C语言习惯,而且《一本通》的许多参考代码都是这种风格:
#include <cstdio> #include <cstring> #include <iostream> using namespace std; const int MAXN = 205; int a[MAXN], b[MAXN], c[MAXN]; char sa[MAXN], sb[MAXN]; int main() { while (scanf("%s %s", sa, sb) != EOF) { memset(a, 0, sizeof(a)); memset(b, 0, sizeof(b)); memset(c, 0, sizeof(c)); int la = strlen(sa); int lb = strlen(sb); // 倒序存储 for (int i = 0; i < la; i++) { a[i] = sa[la - 1 - i] - '0'; } for (int i = 0; i < lb; i++) { b[i] = sb[lb - 1 - i] - '0'; } int lc = max(la, lb); for (int i = 0; i < lc; i++) { c[i] += a[i] + b[i]; // 注意这里用 +=,因为c[i]可能已经有进位 c[i + 1] += c[i] / 10; // 把进位交给下一位 c[i] %= 10; // 当前位保留个位 } // 如果最高位产生了进位,结果长度加1 if (c[lc] > 0) { lc++; } // 倒序输出 for (int i = lc - 1; i >= 0; i--) { printf("%d", c[i]); } printf("\n"); } return 0; }这版代码里最容易被写错的是中间那段竖式加法:
c[i] += a[i] + b[i]; c[i + 1] += c[i] / 10; c[i] %= 10;很多初学者会写成:
c[i] = a[i] + b[i]; c[i + 1] = c[i] / 10; c[i] = c[i] % 10;错在哪里?因为c[i]可能已经不是0了,上一位进位可能已经存进来了。比如计算99+1的时候,第0位相加得10,产生进位1给第1位;第1位还要继续和加数第1位的0相加,同时加上这个进位。如果第1位直接赋值而不是累加,进位就被覆盖丢了。
所以务必记住:c[i]和c[i+1]都要用+=,不是=。这一个符号之差,就是AC和WA的区别。
3.3 边界测试样例与输出细节
写完代码,先用几组特殊的边界数据自测,别急着提交。下面的数据我每次教高精度都会让学生先跑一遍:
| 输入 | 期望输出 | 测试意图 |
|---|---|---|
0 0 | 0 | 全零边界 |
5 5 | 10 | 产生进位的两位数 |
99999999999999999999 1 | 100000000000000000000 | 全九加一,最高位进位 |
12345 0 | 12345 | 一个数为0 |
00001 2 | 3 | 输入带前导零 |
最后一种情况容易被忽略。有的题目输入故意带前导零,比如0001,如果直接按字符串长度处理,结果可能是错的。实际处理时可以考虑把前导零去掉,或者按原样参与运算,但输出时不允许保留前导零。需要根据题目要求判断,如果题目没说,稳妥做法是遇到前导零就过滤掉。
输出采用printf("%d", c[i])时,因为整数数组里存的已经是真正的数字0~9,直接按整数打印没问题。如果用字符数组存结果,也要注意使用%c配合+ '0',别弄混。
4. 常见问题与排查实录
4.1 五个最容易让人WA的写错点
做高精度加法最常见的错误,基本就集中在下面这几个地方。我把它们整理成了一张速查表,提交之前对照自查一遍:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 样例过了但提交全WA | 数组开太小,越界访问 | 开MAXN=205甚至更大 |
| 输出像是反的 | 忘记反转字符串或反向输出 | 存储时就倒序,输出时再反着来 |
| 结果少了最高位 | 循环结束没处理最后的进位 | 进位不为0时继续循环,if (c[lc] > 0) lc++ |
| 多位进位算错 | 把+=写成了= | 用c[i] += a[i] + b[i]和c[i+1] += c[i] / 10 |
| 字符和数字混用 | 忘了'0'的ASCII码 | 统一用- '0'转数字,+ '0'转字符 |
还有一个不那么显眼但特别容易在循环里踩的坑:string的size()返回的是无符号整数,如果写成for (int i = a.size() - 1; i >= 0; i--),当i变成-1时,和size()比较的隐藏类型转换可能带来诡异的问题。这时候要么用int提前接收长度,要么把循环写成while (i >= 0)的形式。字符串版代码里我用int i = a.size() - 1显式转换,就是避免这个坑。
4.2 边界数据自测清单
提交之前,强烈建议把下面这几组测试数据跑一遍:
- 零测试:
0 + 0,验证无进位情况下代码不崩。 - 等长测试:两个长度相同的数相加,比如
1234 + 5678 = 6912。 - 不等长测试:
999 + 1 = 1000,短数处理完后还要继续处理长数。 - 极限进位测试:
99999999999999999999 + 99999999999999999999,连串进位,检验进位循环是否彻底。 - 大数乘2测试:
99999999999999999999 + 99999999999999999999如果做对,乘2问题也就解决了大半。
在这些数据上跑通之后,基本能排除90%以上的错误。剩下的偶然错误,多是因为题目输入格式细节,比如多组数据、文件读写、每行结尾要不要换行等,这些就要看具体题目要求了。
5. 从1223延伸:高精度四则运算与竞赛策略
5.1 高精度减法和乘法怎么套模板
如果1223加法学透了,后面的兄弟题做起来就顺了。因为核心思维是同一个:拆位、倒序、模拟竖式。
高精度减法的关键点有两个:
- 先比较两个数的大小,保证被减数大于等于减数,如果小于,就在最后输出负号;
- 逐位相减时,如果当前位不够减,就要向高位借1,借1当10。
这些逻辑需要在数组层面实现。比较大小不能直接比字符串长度,而是先比长度,长度相同再按位比较字典序。
高精度乘法稍微复杂一点,但思路也很清晰。两个数相乘,第i位和第j位相乘的结果,应该加到结果的i + j位上。整理成代码核心只有两步:
for (int i = 0; i < la; i++) { for (int j = 0; j < lb; j++) { c[i + j] += a[i] * b[j]; } } // 最后统一处理进位 for (int i = 0; i < la + lb; i++) { c[i + 1] += c[i] / 10; c[i] %= 10; }不需要在乘法过程中逐个进位,先全部累积,最后统一处理即可。这个思路比一步一进位清晰很多。
高精度除法在初赛阶段一般不要求直接模拟长除法。更常用的做法是二分答案 + 高精度乘法验证,也就是猜一个商,用高精度乘法把它乘回除数,比较结果与被除数的大小关系,再调整猜测。虽然时间复杂度高一些,但实现难度低,思路简单,实战中非常够用。
5.2 竞赛策略:模板派还是现场推派
关于高精度运算,竞赛圈一直存在的争论是:要不要提前把模板背下来?
我的观点是:平时一定自己写,比赛时一定用背熟的模板。这两件事不矛盾。
平时自己写,是为了理解每一位的进位逻辑、数组越界边界,这样即使模板忘了,也能现场推导。比赛时用背熟的模板,是因为高精度运算的代码虽然不长,但细节极多,现场推容易在紧张时写错。一场比赛的时间有限,与其把时间花在反复调试高精度加法上,不如直接默写一个已经测过无数遍的版本。
什么时候该优先背模板?当你准备NOIP、CSP或者各类信息学竞赛时,高精度加减乘法是基本盘,必须在5分钟内无脑写完。这就像学骑自行车,熟练之后不需要再思考怎么保持平衡,手跟脚自己就会动了。
我自己刷《一本通》那会儿,高精度这章前后写了不下十遍。每写一遍,都会发现一个以前没注意到的小毛病:数组没清零、位数算错、输出多了前导零……等这些问题全部改干净之后,再往后做动态规划、图论时,就再也不会被“大数存不下”这种事绊住脚了。
所以这道1223,题目叫An easy problem,但把它当成检验基本功的试金石,一点都不过分。如果你能把上面这些边界情况全部一次写对,那么恭喜你,高精度这一关,算是真正迈过去了。