CC++ & Algorithm

当计算机不会“借”,我们替它借——聊聊高精度减法里的那些细节

你有没有在超市遇到过这种情况:买了一件 23 块 7 的东西,掏出一张 50 块,收银员找给你 26 块 3。你心里默默验算了一下,发现没错。可如果这张 50 块是 500 位的数字呢?收银员大概要当场辞职。

计算机也面临同样的窘境。C++ 里 int 最大约 21 亿,long long 撑死到 9×10¹⁸,再大就溢出了。但现实中我们动辄要处理几百位的数字——比如密码学里的模运算、天文距离的差值、甚至是某些大数竞赛题。这时候就需要高精度减法登场:把数字拆成一位一位存进数组,像手算竖式那样逐位相减,不够减就向高位借。

听起来简单,对吧?但就是这个“借”字,藏着不少门道。

倒着存,才是正经事

先说一个反直觉的操作:我们把数字倒着存。

803 存成 [3, 0, 8],56 存成 [6, 5]。个位在下标 0,最高位在末尾。为什么?因为竖式减法是从个位开始算的,而数组下标从 0 开始天然对齐。如果正着存,两个长度不同的数做减法时,最高位对不齐,你得手动补零、算偏移,代码立刻变得又臭又长。

倒着存之后,a[i] 和 b[i] 永远在同一个“位权”上。下标 0 就是个位,下标 1 就是十位,以此类推。长度不够的那个数,超出部分直接当 0 处理就行:

int vb = (i < b.size()) ? b[i] : 0;

一行代码解决对齐问题。这是高精度运算里最朴素也最有效的设计决策。

借位这件事,程序不会“自动”帮你做

手工算减法时,你脑子里想的是“3 不够减 8,向十位借 1,变成 13 再减”。但在程序里,没有任何东西是自动的。

有一个判断题说:“在高精度减法中,向高位借位后,高位的数字会自动减少 1,不需要额外处理。” 这句话是错的。计算机不会自动帮你完成借位传递,你必须用变量显式记录“我借了”。

核心逻辑就三行:

int diff = va - vb - borrow;
if (diff < 0) { diff += 10; borrow = 1; }
else borrow = 0;

borrow 这个变量承载了“上一位是否向我借过钱”的信息。它的存在意味着:当前位的计算必须先减去上一位的借位,再判断自己够不够减。

这里有个经典的易错点:当前位是 3,减数是 8,借位后当前位变成多少?答案是 13,不是 2,不是 4,更不是 18。因为十进制下借 1 当 10,3 + 10 = 13。很多人会下意识觉得“借了之后应该变小”,但借位是让当前位变大,代价是高位变小——这个方向感一定要建立起来。

连环借位:1000 − 1 的恐怖故事

真正让初学者翻车的,不是单次借位,而是连环借位。

算 1000 − 1 的时候,个位 0 不够减 1,向十位借;十位也是 0,向百位借;百位还是 0,向千位借。千位的 1 借出去变成 0,百位拿到 10 之后借 1 给十位变成 9,十位拿到 10 之后借 1 给个位变成 9,个位最终拿到 10,10 − 1 = 9。结果:999。

这个过程中,借位像多米诺骨牌一样从低位一路传导到高位。如果代码里只处理了“当前位借一次”而没考虑高位借完后变成负数怎么办,结果就会出错。

但好消息是:用 borrow 变量逐位循环的写法天然支持连环借位。因为每一位都在独立判断 diff < 0,借位标记会自动传递下去。你不需要专门写“从千位一路借到个位”的逻辑,循环会帮你搞定。

这就是好的算法设计的魅力:正确的抽象让复杂问题自然消解。

前导零:结果里的“隐形垃圾”

100 − 99 = 1,但如果你倒着存、逐位减,结果数组可能是 [1, 0, 0],倒过来输出就是 001。看起来像某种神秘代码。

所以最后一步必须去掉前导零:

while (c.size() > 1 && c.back() == 0) c.pop_back();

注意 c.size() > 1 这个条件——如果结果本身就是 0,你不能把它删成空数组。

大小判断:减法之前必须先“谈判”

高精度减法还有一个容易被忽视的前置步骤:判断谁大谁小。

如果被减数小于减数,结果就是负数。对于入门阶段,我们通常约定只处理非负结果,所以必须先比较。

比较规则很直觉:

  • 位数不同,位数多的更大(803 三位 > 56 两位)
  • 位数相同,从最高位开始逐位比较,第一个不同的位决定大小
  • 全部相同,则相等

这个判断逻辑写成一个 ge 函数,在减法之前调用。如果 a < b,直接提示“暂不支持负结果”并退出。如果以后要支持负数,就在交换两数后输出一个负号即可——架构上留好了扩展点。

从减法到除法:思维的一跃

减法学明白了,除法还会远吗?

看这道题:输入一个不超过 100 位的大整数,要求输出它除以 13 的商和余数。比如 2132104848488485 ÷ 13,商是 164008065268345,余数是 0。

这道题的本质是什么?逐位试商。

你从最高位开始,每次把当前余数乘以 10 再加上下一位数字,然后看这个数里面有几个 13。这个“几个”就是商的当前位,剩下的就是新的余数。

int cur = 0;
for (int i = 0; i < n; i++) {
    cur = cur * 10 + (s[i] - '0');
    quotient.push_back(cur / 13);
    cur %= 13;
}
// cur 最终就是余数

核心就这几行。你会发现它和高精度减法共享同一个思想:把大数拆成一位一位,逐位处理,用变量携带“上一位留下的信息”。减法里这个变量叫 borrow,除法里它叫“当前余数”。

再看一道更狠的:高精除以高精,求商和余数。输入两个低于 300 位的正整数,比如:

1231312318457577687897987642324567864324567876543245671425346756786867867867
1231312318767141738178325678412414124141425346756786867867867

输出商 999999999748590,余数 179780909068307566598992807564736854549985603543237528310337。

这道题的做法是高精度减法 + 试商的组合:对每一位,不断用减法去“试探”能减多少次除数,减的次数就是商的这一位。本质上,除法就是反复做减法。你把减法理解透了,除法就是它的自然延伸。

几个值得刻进肌肉记忆的点

  1. 倒着存。个位在下标 0,这是所有高精度运算的基石。
  2. borrow 必须显式管理。计算机不会自动借位,每一步都要手动判断和传递。
  3. 借位是加 10,不是加 1。这个方向感建立不起来,后面全错。
  4. 前导零必须删,但至少保留一位。
  5. 减法前先比大小,这是安全网。

高精度减法看起来只是“模拟手算”,但它教会你的东西远不止算法本身:如何设计数据结构让操作变简单(倒着存)、如何用状态变量携带上下文信息(borrow)、如何处理边界情况(前导零、相等、零)。这些思维方式,在你后面学高精度乘法、除法、甚至更复杂的算法时,会一遍又一遍地出现。

借位就像找邻居帮忙,一个不够就找更远的。耐心一点,逐位来,你一定能算对。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

有用 100%没用 0%
点赞的会员
评论0

还没有评论,来抢沙发~

评论加载中...

想系统学习这个知识点?查看完整知识点 →