刷到华为机试 HJ146“谐距下标对”的时候,我盯着题目名看了好一会儿。谐距是什么距离?下标对为什么还要专起一个名词?等我真正把题面读明白,把条件翻译成数学公式之后,才意识到这题最值钱的不是最后的 AC 代码,而是那个从“枚举元素”转向“枚举参数”的思维切换。
这篇文章我不打算只扔一份能过的代码,而是把中间每一步关键推导都掰开讲清楚:谐距的数学本质是什么、暴力做法为什么必然炸掉、正解为什么用两层看似朴素的循环就能把几亿次枚举压到千万级,以及我在实际提交和自测过程中踩过的几个坑。不管你是准备机试,还是在做一些 gcd 相关的计数题,顺着这个思路走一遍,基本就能独立写出这道题。
1. 先拆题:谐距下标对的数学定义
1.1 题干里的谐距到底指什么
原题描述比较绕,我用大白话转述一下。给定一个长度为 n 的正整数数组 a,对于任意下标对 (i, j)(i < j),我们先求出两个数的最大公约数 g = gcd(a[i], a[j]),然后把两个数同时除以 g,得到两个互质的数 p 和 q。如果这两个互质数相差 1,也就是 |p - q| = 1,那么这一对下标就叫做“谐距下标对”。题目要求统计整个数组里满足条件的下标对总数。
举个例子会直观很多。2 和 3,gcd 是 1,除掉之后还是 2 和 3,相差 1,所以它俩是一对。18 和 20,gcd 是 2,除掉之后是 9 和 10,也相差 1,同样是一对。6 和 8,gcd 是 2,除掉之后是 3 和 4,相差 1,这也是一对。反过来看 12 和 20,gcd 是 4,除掉之后是 3 和 5,差为 2,这就不是谐距下标对。
我第一次读完这个定义,脑子里冒出来的问题只有一个:这玩意儿凭什么能快速统计?如果按定义直接算 gcd,那不是 O(n²) 起步吗?事实确实如此,但关键并不在 gcd 本身,而在于这个定义可以继续往下化简。
1.2 等价变形:连续且互质
设两个数为 x 和 y,最大公约数是 g。我们把它们写成:
x = g * p
y = g * q
其中 p 和 q 一定互质,因为公共因子已经被 g 全部提走了。现在题目要求 |p - q| = 1,那说明 p 和 q 其实是两个相邻整数。假设较小的那个是 t,另一个就是 t + 1。于是:
x = g * t
y = g * (t + 1)
这个形式太关键了。它意味着,所有谐距下标对本质上就是“共享同一个因子 g,同时各自除掉 g 之后,剩下的是相邻两个整数”的一对数。由于相邻整数一定互质,所以 g 恰好就是 x 和 y 的最大公约数,反过来只要两个数能写成 gt 和 g(t+1),它们就一定满足谐距条件,不需要再多做一次 gcd 验证。
把条件整个反推回去,还能得到一个更紧凑的等价式:
gcd(x, y) == |x - y|
也就是说,两个数的最大公约数等于它们的差。验证一下 18 和 20:gcd 是 2,差也是 2,成立。再看 9 和 18:gcd 是 9,差也是 9,成立。这个简洁形式也终于解释了“谐距”这个名字:距离(差值)恰好等于公因数,某种程度上就是一种整除关系上的“和谐”。我对名字来源没有官方考据,但按这个式子去记,保证之后不会忘。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力思路为什么会撞墙
2.1 按下标对硬算的复杂度账
很多人拿到题的第一反应就是两层循环,枚举所有 i < j,然后每组都调用一次 gcd 检查。这个方法在 n 很小的时候没有任何问题,但机试里的 n 通常给到 2×10^5。这个时候下标对总数大约有:
n * (n - 1) / 2 ≈ 2×10^10
也就是两百亿对。就算一次 gcd 只做几十次整数运算,总操作量也到千亿级别了。OJ 的时间限制通常是两三秒,这个量级想都不用想,一定超时。我甚至不建议去尝试优化 gcd 的常数,因为在 n 上翻车的问题,常数优化救不回来。
这还只是按“下标对”枚举。如果你天真地想,数组里不同值可能没那么多,我可不可以按“值对”枚举?同样不行。
2.2 按值域硬算也不靠谱
假设值域最大值是 V。最坏情况下 V 也是 10^6,把所有值对都看一遍,数量级是 V^2 / 2,也就是 5×10^11,比刚才还离谱。就算数组去重后只剩 2×10^5 个不同的值,值对数量依然是约 2×10^10,还是致命。
所以问题的本质不是枚举对象选得不好,而是我们用
