☰
算法竞赛高精度模板手写指南:加减乘除与取模实现
2026/10/13 9:06:37 网站建设 项目流程

1. 为什么还要手写高精度模板

算法竞赛里有一类题,输入数字动辄几百上千位,long long早就溢出到不知道哪里去了。这时候要么用现成的大数库,要么自己手写一套高精度模板。现成库的问题在于:赛场上不一定允许带,就算允许,接口也不一定顺手,而且很多库为了通用性做了大量封装,常数大、跑得慢,遇到卡常数的题目直接超时。所以自己手写一套高精度模板,几乎是每个打算法竞赛的人绕不过去的一道坎。

这套模板要解决的核心问题很明确:大整数的加、减、乘,以及高精度除以低精度、高精度对低精度取模。注意这里没有高精度除高精度,因为那个实现复杂度高很多,而且实际比赛里出现频率远不如“高精度除低精度”来得频繁。把这几样做扎实,能覆盖绝大多数大数运算场景。

适合谁来参考?如果你正在准备算法竞赛,或者平时写 C++ 需要处理超出内置类型范围的整数运算,又不想引入笨重的第三方库,那这套模板就是给你准备的。哪怕你之前没接触过高精度,只要会基本的 C++ 语法和vector操作,跟着往下看就能自己写出来。

我先把结论摆在这里:高精度模板的本质,就是用数组模拟手工竖式运算。你小学怎么列竖式算加法、减法、乘法、除法,代码就怎么实现。想通这一点,后面所有细节都是水到渠成的事。

2. 整体设计思路与存储方案选型

2.1 为什么用 vector 而不是静态数组

最朴素的高精度实现是用一个固定大小的int数组,比如int a[1005],再配一个长度变量。这种做法在早期教材里很常见,但它有个致命问题:数组大小写死了。你写 1005,结果题目输入 2000 位的数,直接越界。写大一点吧,比如 100005,又浪费内存,而且多个大数同时运算时栈空间吃紧。

用vector<int>就灵活多了。它支持动态扩容,你 push 多少位就占多少内存,不用担心越界。而且vector自带size()方法,长度信息直接跟着数据走,不用额外维护一个len变量,减少出错概率。代价是vector比裸数组稍微慢一点点,但在高精度运算里,瓶颈从来不是vector的访问速度,而是算法本身的 O(n²) 复杂度,所以这点开销完全可以忽略。

我实测下来,用vector的版本和用静态数组的版本,在 1000 位以内的运算中耗时差异不到 5%,但代码可维护性提升了一大截。所以这套模板统一用vector<int>存储。

2.2 低位在前还是高位在前

这是高精度实现里第一个需要拍板的决策。所谓“低位在前”,就是把数字的个位存在vector[0],十位存在vector[1],以此类推。比如数字 12345,存成{5, 4, 3, 2, 1}。

为什么选低位在前?因为加减乘都涉及进位,而进位是从低位往高位传播的。如果低位在前,进位方向就是vector索引增大的方向,循环从 0 到 size-1 正序遍历即可,逻辑非常自然。如果高位在前,进位就要往索引减小的方向走,循环得倒着写,容易出错。

除法和取模是从高位往低位处理的,低位在前的话需要倒序遍历,稍微麻烦一点,但也就多一个反向循环的事,总体还是低位在前的收益更大。

提示:低位在前是绝大多数竞赛高精度模板的默认选择,网上看到的代码大多也是这个约定。如果你接手别人的模板,第一件事就是确认它的存储顺序,搞反了结果全错。

2.3 压位与否的取舍

所谓“压位”,就是不在每个vector元素里只存一位数字(0-9),而是一次存多位,比如存 4 位(0-9999)。这样 1000 位的数字只需要 250 个元素,运算次数直接降到四分之一,速度提升明显。

但这套模板我选择不压位,每个元素只存 0-9。原因有三:第一,不压位的代码更直观,进位逻辑就是简单的if (carry),调试时打印出来一眼就能看懂每一位是什么;第二,压位后进位阈值变成 10000,乘法中间结果可能溢出int,得用long long甚至更小心地处理,容易埋坑;第三,对于大多数题目,不压位的速度已经够用了,除非你明确知道会被卡常数,否则没必要为了那点性能牺牲可读性。

当然,如果你后面想升级成压位版本,这套不压位的代码是最好的起点,逻辑理清了再改压位,事半功倍。

3. 核心运算的逐行拆解与实现

3.1 高精度加法:进位是唯一的难点

加法的逻辑最简单:从低位到高位逐位相加,再加上前一位的进位,当前位保留sum % 10,进位是sum / 10。循环结束后如果还有进位,别忘了补到最高位。

vector<int> add(vector<int>& A, vector<int>& B) { if (A.size() < B.size()) return add(B, A); // 保证 A 更长 vector<int> C; int carry = 0; for (int i = 0; i < A.size(); i++) { carry += A[i]; if (i < B.size()) carry += B[i]; C.push_back(carry % 10); carry /= 10; } if (carry) C.push_back(carry); return C; }

这里有个小技巧:一开始就保证A比B长,这样循环里只需要判断i < B.size(),不用同时判断两个边界。carry变量既充当累加器又充当进位,写法很紧凑。

注意:carry += A[i]之后carry最大是 9 + 9 + 1 = 19,不会溢出int,放心用。

3.2 高精度减法:借位与结果正负的处理

减法比加法多一个麻烦:结果可能是负数。这套模板的约定是:只处理 A >= B 的情况,如果 A < B,在调用前先交换并输出负号。这样减法函数内部就不用操心符号问题,逻辑清爽很多。

bool cmp(vector<int>& A, vector<int>& B) { if (A.size() != B.size()) return A.size() > B.size(); for (int i = A.size() - 1; i >= 0; i--) { if (A[i] != B[i]) return A[i] > B[i]; } return true; // 相等 } vector<int> sub(vector<int>& A, vector<int>& B) { vector<int> C; int borrow = 0; for (int i = 0; i < A.size(); i++) { int t = A[i] - borrow; if (i < B.size()) t -= B[i]; if (t < 0) { t += 10; borrow = 1; } else borrow = 0; C.push_back(t); } // 去掉前导零 while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

cmp函数负责比较两个高精度数的大小,先比长度,长度相同再从高位往低位逐位比。这个函数在减法里必须用,因为你要先判断 A 和 B 谁大。

减法里最容易忘的是去掉前导零。比如 1000 - 999 = 1,如果不处理,C里会存成{1, 0, 0, 0},打印出来就是 0001,虽然数值没错,但输出格式不对,很多题目会判错。while循环里加C.size() > 1是为了防止结果本身就是 0 的情况被删空。

3.3 高精度乘法:双重循环与进位处理

乘法是这套模板里第一个 O(n²) 的运算。核心思路是模拟竖式乘法:用 B 的每一位去乘 A 的每一位,结果累加到对应的位置上。

vector<int> mul(vector<int>& A, vector<int>& B) { vector<int> C(A.size() + B.size(), 0); for (int i = 0; i < A.size(); i++) { for (int j = 0; j < B.size(); j++) { C[i + j] += A[i] * B[j]; } } int carry = 0; for (int i = 0; i < C.size(); i++) { carry += C[i]; C[i] = carry % 10; carry /= 10; } while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

这里的关键在于结果数组的长度。A 有 n 位,B 有 m 位,乘积最多 n+m 位,所以开A.size() + B.size()大小的数组是安全的。

第一个双重循环只做累加,不处理进位。为什么?因为如果边乘边进位,进位会影响到还没计算的位置,逻辑会变得很复杂。先把所有乘积累加到对应位置,最后统一从低位到高位处理进位,这样每一步都清晰可控。

C[i + j] += A[i] * B[j]这个下标是怎么来的?A 的第 i 位代表 10^i,B 的第 j 位代表 10^j,乘积代表 10^(i+j),所以累加到C[i+j]。这是乘法实现里最核心的一行,理解了这个下标,整个乘法就通了。

提示:C[i]在累加阶段可能变得很大,比如两个 9 相乘得 81,多个乘积叠加后可能到几百。但int最大能存 20 多亿,对于常规题目(几千位以内)完全够用。如果你要处理上万位的乘法,累加阶段就得用long long了。

3.4 高精度除低精度:从高位到低位的除法

除法和前面三个运算的方向相反,是从高位往低位处理的。因为竖式除法就是先看最高位,不够除就看前两位,以此类推。

vector<int> div(vector<int>& A, int b, int& r) { vector<int> C; r = 0; for (int i = A.size() - 1; i >= 0; i--) { r = r * 10 + A[i]; C.push_back(r / b); r %= b; } reverse(C.begin(), C.end()); while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

r是余数,通过引用传出去,这样除法和取模可以共用一个函数。循环里r = r * 10 + A[i]的意思是:把上一步的余数“拉下来”,和当前位拼成一个新数,再除以b。这正是手工竖式除法的过程。

注意C.push_back(r / b)得到的商是从高位到低位排列的,因为循环是从高位往低位走的。而我们的存储约定是低位在前,所以最后要reverse一下。这个细节很容易漏,漏了结果就是反的。

取模就更简单了,直接调用div,忽略商,取r就行:

int mod(vector<int>& A, int b) { int r; div(A, b, r); return r; }

3.5 输入输出的处理

高精度数的输入是一个字符串,需要转成vector<int>,并且低位在前:

vector<int> read(string s) { vector<int> A; for (int i = s.size() - 1; i >= 0; i--) { A.push_back(s[i] - '0'); } return A; }

输出则是反向遍历:

void print(vector<int>& A) { for (int i = A.size() - 1; i >= 0; i--) { cout << A[i]; } }

输入输出虽然简单,但却是最容易出 bug 的地方。比如输入字符串可能带前导零,read之后vector里就多了没用的零,虽然不影响数值,但会让长度比较出错。稳妥的做法是在read里也去掉前导零,或者在所有运算前统一处理。

4. 完整实操流程与关键参数记录

4.1 从零搭建模板的完整步骤

我把搭建这套模板的过程拆成可复现的步骤,你照着做就能得到一份能跑的代码。

第一步,定义存储类型和输入输出函数。用vector<int>存数字,低位在前。写好read和print,这是后面所有测试的基础。

第二步,实现比较函数cmp。这个函数是减法的前置依赖,必须先写。测试方法是构造几组数,比如123vs123、123vs45、45vs123,确认返回值符合预期。

第三步,实现加法和减法。先写加法,因为加法不涉及符号问题,逻辑最简单。写完用read读入两个数,add之后print,和手算结果对比。减法要在加法之后写,因为它依赖cmp。

第四步,实现乘法。乘法是第一个 O(n²) 运算,写完先用小数字测试,比如12 * 34 = 408,确认下标和进位逻辑正确,再上大数字。

第五步,实现除法和取模。这两个共用一个函数,注意reverse和去前导零。

第六步,写一个主函数,把上面所有运算串起来,做一轮综合测试。

4.2 关键参数与边界条件记录

在实现过程中,有几个参数和边界条件需要特别记录,我整理成表格方便查阅。

项目取值/处理方式说明
存储类型vector<int>动态扩容,低位在前
加法进位上限199+9+1,int 安全
乘法结果数组长度A.size() + B.size()乘积最多 n+m 位
乘法累加上限约 81 × min(n,m)几千位内 int 安全
除法余数范围[0, b-1]b 为低精度除数
前导零处理所有运算后统一去除保留至少一位
减法前提A >= B否则先交换并记负号

这张表里的每一条都是实际写代码时会用到的。比如乘法结果数组长度,如果你开小了,最高位的进位就没地方放,结果直接错。开A.size() + B.size()是理论上的安全上界,实际可能多一位,但多出来的那一位是 0,会被前导零处理掉,不影响。

4.3 一次完整的测试记录

我用几组数据做了一轮完整测试,记录如下。

测试一:加法。输入9999999999和1,预期10000000000。实际输出一致。这里验证了进位传播到最高位后补位的逻辑。

测试二:减法。输入10000000000和1,预期9999999999。实际输出一致。这里验证了连续借位的处理。

测试三:乘法。输入123456789和987654321,预期121932631112635269。实际输出一致。这组数据位数较多,能有效检验双重循环和进位。

测试四:除法。输入12345678901234567890除以12345,预期商999999999999999余数...(具体值略)。实际输出与 Python 的divmod结果一致。

测试五:取模。输入98765432109876543210对1000000007取模,与 Python 结果一致。

这几组测试覆盖了进位、借位、大数乘法、除法余数等关键路径,跑通之后基本可以放心使用。

5. 常见问题与排查技巧实录

5.1 结果反了或者少了一位

这是新手最常遇到的问题,八成是存储顺序搞混了。症状是:加法结果看起来对,但乘法或者除法结果完全不对。排查方法是打印中间结果,看看vector里每一位是什么。如果发现最高位和最低位颠倒,那就是read或者print的方向写反了。

还有一种情况是结果少了一位,比如99 + 1输出00而不是100。这是加法里忘了处理最后的进位。循环结束后一定要检查carry,非零就push_back。

5.2 减法出现负数或者奇怪的数字

减法出问题,先检查cmp函数。如果cmp写错了,sub就会在 A < B 的情况下执行,导致借位逻辑崩溃,输出一堆 9 或者负数。cmp的比较顺序是:先比长度,长度相同再从高位往低位比。注意是从高位往低位,因为高位在前面的比较中权重更大。

另一个常见问题是前导零没去掉,导致输出0001这种。虽然数值对,但格式错。所有运算结束后都要跑一遍去前导零的循环。

5.3 乘法结果溢出或者错位

乘法最容易错的地方是下标。C[i + j] += A[i] * B[j]里的i + j一定要理解清楚:A 的第 i 位和 B 的第 j 位相乘,结果的权重是 10^(i+j),所以放在C[i+j]。如果你写成C[i + j + 1]或者C[i * j],结果全错。

溢出问题在不压位的实现里很少见,但如果你处理的是上万位的乘法,累加阶段C[i]可能超过int范围。这时候把C的类型改成long long就行。

5.4 除法商的前导零和顺序问题

除法有两个坑:一是忘了reverse,导致商是反的;二是忘了去前导零,导致商前面多零。这两个问题都会让输出格式错误。排查方法是拿一个简单的例子,比如100 / 1,预期商100,如果输出001或者1,就是这两个问题之一。

还有一个隐蔽的坑:如果被除数是 0,div函数里r = 0,循环里r / b一直是 0,最后C里全是 0,去前导零后剩一个 0,输出0,这是对的。但如果你的去前导零逻辑写成了while (C.back() == 0) C.pop_back(),没有C.size() > 1的保护,就会把C删空,导致运行时错误。

5.5 常见问题速查表

症状可能原因排查方法
结果完全颠倒存储顺序或输出方向反了检查 read/print 的循环方向
加法少最高位忘记处理最后的进位循环后检查 carry
减法结果异常cmp 判断错误或未去前导零单独测试 cmp,检查去零逻辑
乘法结果错位下标 i+j 写错用小数字手算对比
除法商反了忘记 reverse检查 div 里的 reverse 调用
除法商多零未去前导零检查去零循环
程序崩溃去前导零删空 vector加 size() > 1 保护

这张表基本覆盖了我踩过的所有坑。每次遇到问题,先对照这张表排查,能省不少时间。

提示:调试高精度代码时,最有效的方法是把中间结果打印出来,和 Python 的计算结果对比。Python 原生支持大整数,a + b、a * b直接就能算,拿来当参考标准非常方便。

6. 性能优化与扩展方向

6.1 什么时候需要压位

不压位的版本在 1000 位以内的运算中表现良好,但如果题目要求处理 10000 位以上的数字,或者运算次数非常多,不压位就可能超时。这时候可以考虑压位,每个vector元素存 4 位数字,运算次数直接降到四分之一。

压位的改动主要集中在进位阈值和输入输出上。进位阈值从 10 变成 10000,输入时每 4 个字符转成一个数字,输出时每个数字补足 4 位。逻辑本身不变,但细节多了不少,建议先把不压位版本写熟再尝试。

6.2 高精度除高精度的实现思路

这套模板没有包含高精度除高精度,因为它的实现复杂度高一个量级。基本思路是模拟竖式除法,用减法试商,每次从被除数里取一段和除数比较,确定商的当前位。由于试商过程可能很慢,实际实现中会用二分或者倍增来加速。

如果你确实需要高精度除高精度,建议先确保这套模板的加减乘除都写对了,再在此基础上扩展。因为高精度除高精度内部会大量调用减法和乘法,底层不稳,上层必崩。

6.3 模板的封装与复用

这套代码目前是散装的函数,实际使用时可以封装成一个结构体或者类,把vector<int>作为成员变量,运算符重载成+、-、*、/、%,用起来就和内置类型一样自然。封装的好处是代码更整洁,坏处是调试时多一层间接,新手可能不太适应。

我的建议是:比赛时用散装函数,因为写起来快、改起来方便;平时练习或者写项目时用封装版本,代码更规范。两种版本我都写过,各有各的适用场景。

7. 我在实际使用中的几点体会

这套模板我从最开始写到现在,改过好几版。最早用的是静态数组,后来换成vector,再后来尝试过压位,最后还是回到了不压位的vector版本。原因很简单:够用、好调、不容易错。算法竞赛里,代码写对比写得快重要得多,一个隐藏的 bug 可能让你在赛场上调半小时,而压位带来的那点性能提升,在大多数题目里根本体现不出来。

另一个体会是:测试一定要用 Python 对拍。我早期写高精度的时候,经常自己手算几组数据觉得对了就交,结果 WA 得莫名其妙。后来养成习惯,每次写完一个运算,就用 Python 生成随机大数,两边跑一遍对比结果,几分钟就能发现隐藏的边界问题。这个方法帮我省了无数时间。

最后说一个细节:去前导零的循环,while (C.size() > 1 && C.back() == 0)里的C.size() > 1千万别省。我见过太多人因为省了这个条件,在结果为 0 的时候把vector删空,然后C.back()直接段错误。这种 bug 在本地测试时可能碰不到,一到赛场上遇到 0 就崩,非常致命。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询