点开赛题列表,看到“结果可能很大,请使用高精度”、“输出不超过 XXX 位的整数”、“答案需要对 10^9+7 取模”这些字眼,心里就要有数了:高精度这关,蓝桥杯一定会考。不管你是 C/C++ 组还是 Java/Python 组,大整数运算都是一道绕不过去的坎。区别只在于,Java 有现成的 BigInteger,Python 的 int 天生无限长,而 C/C++ 选手必须自己手写一套高精度模板。这篇就把我整理多年、在蓝桥杯赛场上实际验证过的模板和踩坑经验一次性分享出来,适合备赛选手、算法初学者,以及所有被大数问题折磨过的人。
1. 为什么高精度是蓝桥杯绕不开的考点
1.1 数据范围决定你必须会高精度
先看一个很基础的问题:int 能装多大?大约是 21 亿,也就是 2.1×10^9。long long 能装 9.2×10^18,看起来很大,但阶乘涨得更夸张。50! 大约是 3×10^64,已经远超 long long;100! 更是达到了 9×10^157。而蓝桥杯里出现阶乘、斐波那契数列、组合数、大数幂的题目,数据范围轻轻松松就让你超过 long long 的边界。
举几个典型场景:
- n 达到 1000 的阶乘求和,结果有几万位。
- 递推题里第 n 项是个大数,题目根本不要求取模,直接要求输出完整结果。
- 组合数 C(n, k) 的精确值,n 到 500,结果同样是天文数字。
这些题的本质就是高精度运算。哪怕题目本身是考动态规划、递推、贪心,只要中间结果爆类型,你就必须用高精度把计算接住。
1.2 不同语言组别的高精度待遇
这里必须说清楚一件事:蓝桥杯按语言分组,高精度的“待遇”完全不同。
Python 选手最轻松,因为 Python 的 int 是任意精度,直接相加相乘就行,不需要自己实现。Java 组有 BigInteger 类,提供 add、subtract、multiply、divide、mod、compareTo 全套方法,使用起来也比较无脑。但 C/C++ 组没有内置大整数,必须手写。
这就导致一个很有趣的现象:同样的题目,Python 选手几行写完,C++ 选手还在敲模板。但这不代表 C++ 组吃亏,因为 C++ 手写高精度在性能上通常比 Python 大整数更快,尤其在做压位优化后,万位级别的数字运算,C++ 明显占优。
无论你报哪个组,我都建议理解高精度的底层原理。原因很简单:有些题目会对大数做特殊变换,比如大数取模、大数开方、高精度小数运算,这时候原生 API 不一定顺手,你得知道内部是怎么回事才能改得动。
1.3 高精度题的三种出场方式
根据我刷真题和模拟题的经验,高精度在蓝桥杯里很少“单独出道”,更多是作为其他算法题的组成部分。常见的有三种:
第一,填空压轴题,直接要求你输出某个巨大数值。这种题往往没有太复杂的算法,核心就是高精度模板的正确性。
第二,综合题里的中间计算。比如某道模拟题求大数阶乘的末位非零数字,或者求大数幂的值,先要完整算出大数,再从中提取信息。
第三,和数论结合,比如需要计算大组合数精确值,再配合取模、质因数分解等操作。
无论哪种出场方式,你都需要一套正确、稳定、能快速写出来的模板。这不是说让你死记硬背,而是要做到肌肉记忆,考场上不假思索就能敲出来。
2. 高精度的核心设计:为什么数字要倒着存
2.1 模拟竖式,从低位开始
高精度算法的本质就是模拟手算竖式。手算加法时从个位开始加,满十进一;减法从个位开始借位;乘法也是从低位到高位逐位相乘。因此,数字在数组里的存储顺序很关键。
我习惯用 vector 存储,下标 0 存个位,下标 1 存十位,以此类推。比如数字 12345,在数组里就是 {5, 4, 3, 2, 1}。这种“低位在前”的存储方式有几个好处:
- 进位和借位都在数组尾部扩展,不需要频繁移动元素。
- 长度不同的两个数比较、相加时,从下标 0 开始处理,逻辑更统一。
- 乘法累加时,a[i] * b[j] 的结果正好加到 c[i+j] 的位置,公式简洁直观。
如果你反过来“高位在前”,也不是不行,但进位时就得在头部插入,效率低,代码也更别扭。我见过不少新手写反存储方向,导致加减乘除四个函数全部出 bug,排查起来非常痛苦。
2.2 字符串输入与数组转换
大数只能用字符串读入,因为没有任何整型能装下几千位的数字。转换过程很简单:从字符串末尾往前遍历,每次 s[i] - '0' 得到对应数字,push 进数组。
这里有一个容易出错的细节:输入字符串可能带前导零,比如 “00123”。如果直接转换,数组里会多出两个 0,后续计算没问题,但输出时就会出现前面多打印零的情况。更严谨的做法是转换后调用一个去除前导零的函数。
#include <bits/stdc++.h> using namespace std; // 去除前导零,保证数组至少保留一位 void trim(vector<int>& a) { while (a.size() > 1 && a.back() == 0) a.pop_back(); } // 字符串转高精度数组,低位在前 vector<int> str2num(const string& s) { vector<int> a; for (int i = (int)s.size() - 1; i >= 0; --i) { a.push_back(s[i] - '0'); } trim(a); return a; } // 比较两个非负大数的大小 // 返回 1 表示 a>b,0 表示相等,-1 表示 a<b int cmp(const vector<int>& a, const vector<int>& b) { if (a.size() != b.size()) { return a.size() > b.size() ? 1 : -1; } for (int i = (int)a.size() - 1; i >= 0; --i) { if (a[i] != b[i]) return a[i] > b[i] ? 1 : -1; } return 0; }trim 函数必须注意一个边界:如果数字是 0,数组是 {0},不能把最后一个 0 也去掉。所以 while 条件是 size() > 1,至少保留一位。
2.3 压位:用空间换时间的实用技巧
基础模板用十进制,每一位存一个 0 到 9 的数字,直观但浪费空间。举个例子,计算 10000!,结果大约有 38660 位,你要处理四万次循环。如果压位,把每 4 位数字打包成一个 int 存储,数组长度直接缩小到原来的四分之一,运算循环次数也大幅降低。
压位的做法是设置 BASE = 10000,比如数字 123456789,在万进制下表示为 {6789, 2345, 1},因为从低位开始每 4 位一组:6789、2345、1。
压位后的加减乘除逻辑和十进制完全一样,只是进位和借位的界限从 10 变成了 10000。但有一个关键坑:乘法累加时,c[i+j] 可能累积相当大的值。如果 BASE 取 10000,一位乘以一位最大是 9999×9999 ≈ 10^8,累加几千次就超过 int 范围。所以压位乘法时,c 数组要么用 long long 存储,要么在每步取模时小心处理。我习惯 BASE 取 10000,配合 long long 做进位,稳当且性能足够。
压位后的输出也和普通不同,不能直接逐个数字打印。因为数组里每个元素代表 4 位,最高位那段可能不足 4 位,直接打印没问题;但中间的段如果不足 4 位,必须补前导零,否则数字就“变短”了。正确做法是:先打印最高位,其余位用 printf("%04d") 补零。
const int BASE = 10000; void printPacked(const vector<int>& a) { printf("%d", a.back()); for (int i = (int)a.size() - 2; i >= 0; --i) { printf("%04d", a[i]); } printf("\n"); }压位模板我建议在备赛后期再掌握,先把十进制模板练熟。因为本质上两者只是 BASE 不同,进位逻辑一模一样。如果你连十进制都写不溜,贸然压位只会增加调试难度。
3. 实战模板:高精度四则运算核心代码
3.1 加法:模拟竖式,记得最后进位
加法是最简单的高精度运算。从最低位开始,逐位相加,加上进位,结果对 10 取余作为当前位,除以 10 作为新的进位。循环结束后,如果进位还有值,别忘补到最高位。
vector<int> add(const vector<int>& a, const vector<int>& b) { vector<int> c; int carry = 0; int n = max(a.size(), b.size()); for (int i = 0; i < n; ++i) { if (i < (int)a.size()) carry += a[i]; if (i < (int)b.size()) carry += b[i]; c.push_back(carry % 10); carry /= 10; } if (carry) c.push_back(carry); return c; }这个模板不需要先对较短的数组补零,直接在循环里通过 i < a.size() 判断,简洁不容易错。一个小测试:999 + 1,循环三次,carry 依次是 10、10、10,每一位都是 0,最后 carry=1 补进最高位,结果得到 1000,完美。
3.2 减法:先比较大小,再借位
减法的核心难点是处理负数和借位。我的建议是:自己实现的减法函数默认要求 a >= b,也就是只做非负数的减法,返回 a - b 的绝对值。调用前先用 cmp 函数判断大小,如果 a < b,就交换参数并标记结果为负数。
借位逻辑:当前位是 a[i] - 借位,如果 i < b.size() 还要再减去 b[i]。如果结果小于 0,就加 10 并向高位借 1;否则借位清零。
// 要求 a >= b(调用前用 cmp 判断) vector<int> sub(const vector<int>& a, const vector<int>& b) { vector<int> c; int borrow = 0; for (int i = 0; i < (int)a.size(); ++i) { int cur = a[i] - borrow; if (i < (int)b.size()) cur -= b[i]; if (cur < 0) { cur += 10; borrow = 1; } else { borrow = 0; } c.push_back(cur); } trim(c); // 去掉前导零,但如果结果是 0,保留一位 0 return c; }有人会问:如果 a == b,结果是 0,trim 之后数组变成 {0},完全正确。但如果 a > b 且从最高位一直借位到结束,循环结束时 borrow 理论上一定是 0,因为这是 a >= b 的前提保证。如果调用前没做大小判断,这里就会出现 borrow 为 1 的诡异情况,结果变成负数但数组只存了绝对值,所以在外面把好关很关键。
3.3 乘法:双重循环与集中进位
高精度乘法也是模拟竖式,但不是逐位动态进位,而是先做一个“[卷积]”式的累加,再统一进位。这里我用了一个小技巧:先不管进位,把每一位的乘积累加到 c[i+j] 中,所有乘累加结束后,再从低位到高位集中处理进位。
vector<int> mul(const vector<int>& a, const vector<int>& b) { vector<int> c(a.size() + b.size(), 0); for (int i = 0; i < (int)a.size(); ++i) { for (int j = 0; j < (int)b.size(); ++j) { c[i + j] += a[i] * b[j]; } } // 集中进位 for (int i = 0; i < (int)c.size() - 1; ++i) { c[i + 1] += c[i] / 10; c[i] %= 10; } trim(c); return c; }为什么用这种“先累加后进位”的方式?因为如果把进位放在内层循环里,每加一次就要取模一次,代码更繁琐;而集中进位的循环次数是 O(n + m),即使累加值比较大,也能一次性处理干净。
实际比赛里,高精度乘高精度的场景没有想象中那么多。更多时候是一个高精度数乘以一个普通 int,比如阶乘。这时候写一个 mulSmall 函数,配合单次进位,效率高得多。
vector<int> mulSmall(const vector<int>& a, int b) { if (b == 0) return vector<int>{0}; vector<int> c; int carry = 0; for (int i = 0; i < (int)a.size(); ++i) { carry += a[i] * b; c.push_back(carry % 10); carry /= 10; } while (carry) { c.push_back(carry % 10); carry /= 10; } trim(c); return c; }mulSmall 的进位逻辑是:当前位的结果是 a[i] * b 加上之前的进位,对 10 取余留下当前位,除以 10 进入下一轮。循环结束后进位可能不止一位,用 while 一直往高位推。
3.4 除法:除以单精度与大数除以大数
除法是高精度运算里最容易劝退人的部分。我分两个难度来讲。
高精度除以单精度(int 除以 int 的除数)比较简单,直接从最高位开始,模拟长除法:当前余数乘 10,加上当前位数字,除以除数得到商的一位,取余作为新的余数。
// 高精度 a 除以普通整数 b,商返回,余数通过 r 带回 vector<int> divSingle(const vector<int>& a, int b, int& r) { vector<int> c(a.size(), 0); r = 0; for (int i = (int)a.size() - 1; i >= 0; --i) { r = r * 10 + a[i]; c[i] = r / b; r %= b; } trim(c); return c; }注意这里的循环是从数组末尾向开头遍历,也就是从数字的最高位向最低位处理。因为除法的竖式是从高位开始的,和加减乘正好相反。如果这里反了,结果完全错误。
高精度除以高精度就复杂了,蓝桥杯现场如果没有现成模板,我建议直接采用“逐位试商”的方法:从被除数的最高位开始,每轮把当前位“补”到余数后面,然后从 9 到 0 依次试探,找到最大的 k 满足 k × b ≤ 当前余数。把 k 写入商,用当前余数减去 k × b 得到新的余数。本质上就是小学长除法,只不过每一步的乘法和减法都用高精度实现。
vector<int> divBig(const vector<int>& a, const vector<int>& b) { vector<int> q; // 商,暂存高位到低位 vector<int> rem; // 余数 for (int i = (int)a.size() - 1; i >= 0; --i) { // rem = rem * 10 + a[i],相当于在数字右侧补上一位 rem.insert(rem.begin(), a[i]); trim(rem); int k = 0; vector<int> prod; // 从 9 到 1 试商 for (int d = 9; d >= 1; --d) { prod = mulSmall(b, d); if (cmp(prod, rem) <= 0) { k = d; break; } } if (k) rem = sub(rem, prod); q.push_back(k); } reverse(q.begin(), q.end()); trim(q); return q; }这个模板有一个前提:调用前要判断 b 不为 0。另外,它的时间复杂度是 O(n × 9 × m),对于长度几千位的数字来说,蓝桥杯的数据范围完全够用。如果被除数和除数都上万位,就需要更高级的优化了,但那种题在蓝桥杯里基本不会出现。
3.5 负数怎么处理
很多选手写高精度只考虑非负数,遇到负数就懵。我的处理方式很简单:用一个 bool 变量记录符号,vector 只存绝对值。四则运算时按符号分类:
- 加法:同号则绝对值相加;异号则比较绝对值大小,大减小,符号取绝对值大的一方。
- 减法:a - b 等价于 a + (-b),把减法统一到加法的逻辑上。
- 比较大小:先看符号,负数小于非负数;同符号时再比较绝对值。
比赛里真正要求你处理负数的场景比较少,但“求差”这类题经常出现,所以至少要把“先比大小再决定正负”这个逻辑写好。我在本地模板里会额外写一个 signedAdd 和 signedSub 的封装,直接用 bool sign 存储符号,避免每次都在主逻辑里手写判断。
4. 典型场景还原:蓝桥杯高频题型与实战思路
4.1 大数阶乘与阶乘和
大数阶乘是蓝桥杯的高频考点,比如要求输出 n! 的所有位,n 可以到 1000 甚至 10000。直接用 mulSmall 从 1 乘到 n 就行,每乘一个 i,就是在原有高精度结果上乘以一个普通整数,完全不需要高精度乘高精度。
vector<int> factorial(int n) { vector<int> res{1}; for (int i = 2; i <= n; ++i) { res = mulSmall(res, i); } return res; }求阶乘和(1! + 2! + ... + n!)更常见,如果对每个 k 单独调用 factorial(k),复杂度是 O(n²),虽然数据范围不大时也能跑,但没必要。更好的做法是递推:维护当前阶乘 cur,每次 cur = mulSmall(cur, i),然后 sum = add(sum, cur)。这样只需要一次高精度乘法和加法循环,快得多。
4.2 大数幂运算
有些题要求输出 2^n 的完整值,n 可能到几百甚至几千。不要一上来就写高精度乘高精度的快速幂,因为 2^1000 只有 302 位,直接循环乘 2 一千次完全没问题。
但如果 n 是几百万,循环乘 2 就太慢了。这时候可以考虑按位数估算:如果结果位数超过几万,单纯的循环乘法会越来越慢,因为位数线性增长,总复杂度是 O(n²)。这种情况下,用快速幂配合高精度乘高精度(mul 函数)是更好的选择,快速幂的乘法次数只有 O(log n)。
不过我要提醒一点:蓝桥杯的数据范围往往经过设计,如果你发现乘 2 的循环次数高达十万、百万,那大概率题目有别的技巧,比如只输出模意义下的结果或者末几位,而不是让你暴力算完整个大数。
4.3 大组合数精确值
组合数 C(n, k) 的精确值经常出现在概率、方案数的题目中。当 n、k 很大时,long long 根本装不下。精确计算组合数有一个常用递推公式:
C(n, k) = C(n, k-1) × (n - k + 1) / k
翻译成高精度操作就是:用 mulSmall 乘以 (n - k + 1),再用 divSingle 除以 k。因为递推过程中每一步的结果都能被 k 整除,所以不用担心小数。
// 返回 C(n, k) 的精确值,要求 0 <= k <= n vector<int> combination(int n, int k) { if (k > n - k) k = n - k; vector<int> res{1}; for (int i = 1; i <= k; ++i) { res = mulSmall(res, n - k + i); int r; res = divSingle(res, i, r); } return res; }这个模板在我的备赛笔记里出现过很多次,解决了好几道组合计数题。
4.4 高精度小数的特殊情况
严格来说,“小数高精度”在蓝桥杯很少单独出现,但偶尔会碰到需要高精度计算到小数点后若干位的题。我的处理思路是把所有数字统一放大成整数来处理,比如保留 6 位小数,就把所有数值乘 10^6,最后输出时在正确位置补小数点。这样就能复用整数高精度的加减乘除模板,不需要另写一套。
这种“定点数”思路本质上是把浮点问题转化为整数问题,避开浮点误差。如果你遇到乘法,注意放大倍数会平方增长,比如保留 6 位小数的两个数相乘,结果放大倍数是 10^12,最后要缩小回去,别搞混。
5. 调试、避坑与现场保分策略
5.1 三个我踩过的低级错误
第一个错误是字符串转数组时下标顺序搞反。刚开始写 str2num,有人会顺手写成 for (int i = 0; i < s.size(); ++i) a.push_back(s[i] - '0'),读字符串 “123” 变成数组 {1,2,3},即数字 321,后面所有运算全乱。我建议在 str2num 里写一行注释:低位在前,切记。
第二个错误是减法结果忘记处理前导零。10 - 9 = 1,但如果你的数组是 {0, 1},不 trim 的话输出就变成 01。尤其当结果为 0 时,数组会变成 {0, 0, 0 ...},一 trim 又把全部零去掉,数组变成空。所以 trim 的终止条件是 size() > 1,不是 size() > 0。
第三个错误是压位输出时没有补零。BASE=10000 压位后,数组 {1, 2, 3} 表示的数字是 3×10000² + 2×10000 + 1 = 300020001,但如果最高位后面的段不补零,直接打印就变成 3121,相差十万八千里。这是压位模板最容易翻车的地方。
5.2 边界数据与评测点
高精度模板写完后,千万别直接交。我习惯在本地跑一组固定测试用例:
- 999 + 1,检验加法最高位进位。
- 1000 - 1,检验减法借位链。
- 999 × 999,检验乘法进位集中处理。
- 123456789 / 12345,对比 Python 计算结果,检验除法。
- 0 + 0、0 - 0、0 × 999,检验全零边界。
这些用例几乎覆盖了评测系统最容易埋的边界陷阱。只要这几个都能通过,再大的数字基本也不会出问题。
5.3 怎么用对拍验证模板
更严谨的验证方式是对拍:用 Python 写一个用原生大整数计算结果的小脚本,生成随机测试数据,再用你的 C++ 高精度程序算一遍,逐项比对输出。
对拍脚本不需要多复杂,核心三步:
- Python 随机生成两个大数和运算符。
- 分别调用 Python 内置运算得出正确答案,以及你编译好的 C++ 程序得出待测答案。
- 逐行比较,一旦不一致,立刻输出测试数据和两个答案。
这个方法能帮你在赛前发现模板里隐藏的 bug,强烈建议每个人都在备赛时做一次。我用这个方法至少揪出了三次隐藏的边界错误,都是靠手测很难发现的。
5.4 考场上写不出高精度除法怎么办
最后说一个很现实的保分策略:高精度除法,尤其是高精度除以高精度,是整套模板里最复杂、最容易写错的部分。如果比赛现场发现时间不够,不要慌,分情况处理。
如果题目只需要商和余数中的一种,并且除数也是大数,优先检查有没有可能用“高精度除以单精度”特判部分数据,比如数据范围有 30% 的小数据,先把这部分分拿稳。如果是求精确结果,采用逐位试商法,代码量其实不大,关键是平时练熟。
还有一个取巧的办法:有些题目求两个大数的整除结果,可以利用二分答案。二分商 q,每次用 mul(b, q) 判断 q×b 是否 ≤ a。这种做法跑得比逐位试商慢,但胜在逻辑简单、不容易写错,用来拿部分分或者应对不常见的数据范围绰绰有余。
我个人备赛时的习惯是把高精度整套模板放在代码库的固定位置,每次模拟赛前先默写一遍加法和乘法,再顺手写减法、除法。默写几次之后,考场上根本不需要想,手指比脑子快。真正理解了竖式原理之后,你会发现高精度没有想象中那么可怕,它就是一个稍微繁琐一点的模拟题。把这些模板练成条件反射,蓝桥杯的大数题对你来说就是送分题。