看到这类题目,很多人的第一反应是掏出IDE准备暴力循环,但真正动手之后会发现两个问题:一是循环上界设多少完全没谱,二是就算跑出结果也不敢保证唯一。这道“整数加100和加168都是完全平方数”的经典题,其实藏着非常漂亮的数学结构,用Java实现时也有不少值得讲的细节。这篇文章我会从题目拆解开始,给出三种可落地的Java解法,再把判断完全平方数时容易踩的浮点精度坑、溢出坑都摊开讲清楚。1. 先把题目拆透:这道题到底在问什么1.1 变量与条件的数学翻译题目原文是:一个整数,它加上100后是一个完全平方数,加上168又是一个完全平方数,请问该数是多少?先别急着写代码,把自然语言翻译成数学语言。设这个整数为 x,那么存在两个整数 m 和 n,使得:x + 100 = m²x + 168 = n²这里有一个很关键的顺序问题:两个条件是“分别加上100和168”,不是“加上100之后,再在结果上加上168”。如果你把第二个条件理解成 x + 100 + 168 = n²,那整道题的答案会完全不一样,后面我会专门讲这个表述陷阱。既然是同一个 x,那么两个式子相减是最自然的操作:(x + 168) - (x + 100) = n² - m²化简后就是:n² - m² = 68到这里题目已经从“找一个整数 x”转化成了“找两个整数 m、n,使它们的平方差等于68”,然后 x = m² - 100 自然就出来了。这一步转化是整个题目的灵魂,也是面试官最想看到的思维过程。1.2 为什么“68”这么关键68这个数字不是随便来的,它就是两个条件之间的差的绝对值。168 - 100 = 68,就这么简单。但恰恰是这个差值决定了题目的解法和解的个数。平方差公式告诉我们:n² - m² = (n - m)(n + m)所以问题又变成了:把68拆成两个整数相乘,并且这两个因数恰好等于 n - m 和 n + m。这就把一个看似无限的枚举问题,变成了一个有限范围内的因数分解问题。68的因数对不多,也就几组,手算都能算出来,编程做更是秒杀。1.3 先别急着写代码:手推一遍答案在打开编辑器之前,我建议你先拿纸笔推一遍。这个过程会让你对解法有真正的理解,而不是背代码。68的因数对包括:1 × 682 × 344 × 17注意,n - m 和 n + m 必须同奇偶,因为它们的和是 2n,一定是偶数;它们的差是 2m,也一定是偶数。所以如果两个因数一个是奇数一个是偶数,直接排除。看一遍:1 和 68,一奇一偶,排除。2 和 34,两个都是偶数,可行。于是 n - m = 2,n + m = 34,解得 n = 18,m = 16。4 和 17,一奇一偶,排除。所以 m = 16,n = 18,代回 x = m² - 100 = 256 - 100 = 156。验证一下:156 + 100 = 256 = 16²,156 + 168 = 324 = 18²。答案完美。注意:这里用“因数对同奇偶”筛选有一个前提,就是 m、n 我们默认取非负整数。实际上 m 和 n 取负数也不影响平方结果,但因为平方值只看绝对值,最终得到的 x 不变。这个细节在面试中主动提出来,会显得你考虑问题很全面。2. Java解法一:暴力枚举,简单但够用2.1 思路与边界确定最直白的思路是:把 x 的可能取值一个个试过去,每个 x 都检查 x + 100 和 x + 168 是不是完全平方数。问题来了:x 的枚举范围怎么定?下界很明确,因为 x + 168 必须是非负数(完全平方数最小是0),所以 x ≥ -168。上界呢?这是暴力法最尴尬的地方。题目没有限制 x 的范围,理论上它可以非常大。但因为 n² - m² = 68,如果 n > m,那么 n - m ≥ 1(整数之间最小差1),于是:n² - m² = (n - m)(n + m) ≥ 1 × (2m + 1) = 2m + 1也就是说 2m + 1 ≤ 68,解得 m ≤ 33。这其实告诉我们,就算用暴力法,也没必要把 x 枚举到天上去。x = m² - 100,m 最大33,所以 x 最大不超过 33² - 100 = 989。看到没有,上界其实小得惊人。当然,如果你不想在暴力法里做这个推导,也可以拍脑袋给一个比较大的上界,比如十万、一百万。但知道了数学关系之后,你会更安心:枚举到 1000 以内就够了。2.2 基础版代码实现直接上代码:public class BruteForceSolution { public static void main(String[] args) { int count = 0; // 根据 m <= 33 可知 x 最大为 33*33 - 100 = 989 for (int x = -168; x <= 1000; x++) { if (isPerfectSquare(x + 100) && isPerfectSquare(x + 168)) { System.out.println("找到整数: " + x); count++; } } System.out.println("共找到 " + count + " 个"); } private static boolean isPerfectSquare(int num) { if (num < 0) { return false; } int sqrt = (int) Math.sqrt(num); return sqrt * sqrt == num; } }isPerfectSquare的写法是这套代码的核心:先用Math.sqrt开方,再强转成 int 截断小数部分,最后用乘法回验。比如 num = 256,sqrt = 16.0,转 int 还是16,16×16 = 256,相等,返回 true。比如 num = 257,sqrt ≈ 16.03,转 int 变成16,16×16 = 256 ≠ 257,返回 false。输出结果:找到整数: 156 共找到 1 个这个版本代码量最小,逻辑直白,特别适合第一次接触这道题的人。2.3 这版代码的坑:Math.sqrt的浮点隐患暴力解法看着简单,但Math.sqrt返回的是 double,这里藏着几个细节。第一个坑:用Math.sqrt(num)后强转 int,如果 num 本身是负数,Math.sqrt会返回 NaN,强转成 int 的结果在 Java 里是 0。所以isPerfectSquare里必须先做if (num < 0)的防御判断,否则你的逻辑会出错。我见过有人写出(int)Math.sqrt(-1) == 0,然后 0×0 == 0,误判 -1 是完全平方数,这就是典型的防御性编程没做到位。第二个坑:double 的精度问题。当 num 非常大时,Math.sqrt的结果可能是一个很接近整数的浮点数,比如 999999.9999999,强转 int 后变成 999999,回验就可能不相等。更稳妥的做法是判断:double s = Math.sqrt(num); if (s - Math.floor(s) < 1e-10) { // 认为是完全平方数 }但我不推荐这个写法,因为误差阈值难定,不同数量级下表现不稳定。最可靠的永远是“整数回验”方案:转成 int 后乘回来再比较原数。只要 num 不超过 int 范围,这个方案就是精确的。第三个坑:乘法溢出。sqrt * sqrt如果 num 接近 Integer.MAX_VALUE,sqrt 约等于 46340,46340 × 46340 = 2147395600,没溢出。但如果 num 超过 int 范围,或者你把判断逻辑用在了 long 类型上,就可能悄悄溢出变成负数,导致判断结果完全错误。下面一节会专门讲大数场景。3. Java解法二:因数分解,一步到位3.1 从平方差公式到因数对暴力法适合理解题目,但如果你想在面试里展示出数学功底,因数分解法才是真正的标准答案。前面已经推过:n² - m² = (n - m)(n + m) = 68。设 a = n - m,b = n + m,那么 a × b = 68,并且有 b > a > 0(因为 m、n 非负且 n > m,这里先讨论正数情况)。同时,a 和 b 必须同奇偶。理由很简单:a + b = 2n,是偶数,所以 a、b 要么都是偶数,要么都是奇数。而 a × b = 68 是偶数,排除了“两个奇数相乘”的可能,所以 a 和 b 必须都是偶数。这就是一个非常规整的逻辑链:枚举 68 的所有正因数对。过滤掉不同奇偶的组合。过滤掉 a >= b 的组合(因为 n - m < n + m)。解出 n = (a + b) / 2,m = (b - a) / 2。代回 x = m² - 100。3.2 奇偶性筛选的数学理由这一步值得单独说一下。为什么网上很多答案里,看到 68 = 2 × 34 就直接跳过了 1 × 68 和 4 × 17?有人说是“因为 68 的因数里只有 2 和 34 都是偶数”,这其实只是结果不是原因。真正的原因就是我上面写的同奇偶判断。把它讲明白,你会比那些“背答案”的人强很多:如果 a、b 一奇一偶,那么 a + b 是奇数,但 a + b = 2n 必须是偶数,矛盾。如果 a、b 都是奇数,那么 a × b 是奇数,但 a × b = 68 是偶数,矛盾。所以 a、b 只能都是偶数。有了这层推导,你甚至可以给出一个一般性结论:当目标差值 D 为偶数时,如果 D 不是 4 的倍数,那么 D 无法分解成两个偶数的乘积,平方差为 D 的整数对就不存在。比如 D = 2,只能拆成 1×2,一奇一偶,无解。这个扩展思路在后面讲通用解法时非常有用。3.3 Java实现:枚举因数并筛选我把因数分解法的完整代码写出来:public class FactorizationSolution { public static void main(String[] args) { int diff = 168 - 100; // 68 for (int a = 1; a * a <= diff; a++) { if (diff % a != 0) { continue; } int b = diff / a; // 要求 b > a,且 a、b 同奇偶 if (b <= a) { continue; } if ((a & 1) != (b & 1)) { continue; } int m = (b - a) / 2; int n = (a + b) / 2; int x = m * m - 100; System.out.println("a=" + a + ", b=" + b + ", m=" + m + ", n=" + n + ", x=" + x); // 验证 int s1 = m * m; int s2 = n * n; if (s1 == x + 100 && s2 == x + 168) { System.out.println("验证通过"); } } } }这段代码里有个小技巧:循环条件a * a <= diff,意味着 a 只需要枚举到 sqrt(diff) 就够了,因为如果 a 是较小因数,b = diff / a 自动就是较大因数。你不需要枚举到 68,只要 a 从1试到8(因为 8 × 8 = 64 ≤ 68),四组因数对都能覆盖到。运行输出:a=2, b=34, m=16, n=18, x=156 验证通过代码只输出一组结果,数学上也是唯一解,跟手推完全一致。3.4 一个重要扩展:一个数加上100是平方数,再加上168也是平方数我前面提醒过表述陷阱,现在用因数分解法来展示这个陷阱的威力。如果题目改成:“一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数”,那条件就变成:x + 100 = m²x + 100 + 168 = n²两个式子相减得到:n² - m² = 168而不是 68。用同样的因数分解代码,把 diff 改成168:168的因数对(a < b)有:1 × 168,一奇一偶,排除。2 × 84,两个偶数,可行,m = 41,n = 43,x = 41² - 100 = 1581。3 × 56,一奇一偶,排除。4 × 42,两个偶数,可行,m = 19,n = 23,x = 19² - 100 = 261。6 × 28,两个偶数,可行,m = 11,n = 17,x = 21。7 × 24,一奇一偶,排除。8 × 21,一奇一偶,排除。12 × 14,两个偶数,可行,m = 1,n = 13,x = -99。所以这个“改版”题目有四个答案:-99、21、261、1581。你看,仅仅是把“加上168”换成“再加上168”,差值就从68变成168,解的个数也从1个变成4个。这就是为什么读题时一字之差,写代码前务必把数学关系确认清楚。以后不管遇到哪个版本,用因数分解法都能快速解出,还不会漏解。4. Java解法三:倒推双循环,更通用的视角4.1 思路:换个角度枚举暴力法是枚举 x,因数分解法是拆因数,还有第三种思路:直接枚举 m,然后用 n² = m² + 68 去判断。为什么要枚举 m?因为 m 是平方根,它的取值范围比 x 小得多。根据之前的推导,n > m 时:n² - m² = (n - m)(n + m)由于 n - m ≥ 1,n + m ≥ 2m + 1,所以:68 ≥ 2m + 1m 最大只能取到 33。也就是说,只需要从 0 到 33 枚举 m,34次判断就能解决问题。4.2 代码实现与复杂度分析代码长这样:public class EnumerateRootSolution { public static void main(String[] args) { int diff = 168 - 100; for (int m = 0; m <= 33; m++) { long m2 = (long) m * m; // 避免 int 乘法溢出 long n2 = m2 + diff; long n = (long) Math.sqrt(n2); if (n * n == n2) { long x = m2 - 100; System.out.println("m=" + m + ", n=" + n + ", x=" + x); } } } }我来解释几个细节:(long) m * m这一步很关键。如果 m 是 int,m * m 先按 int 计算,一旦超过 2^31 - 1 就会溢出,结果变成负数。虽然这里 m 最大33不会溢出,但培养这种防御习惯很重要,尤其是日后处理大数题目。Math.sqrt(n2)的参数是 long,会自动转成 double。如果 n2 很大,double 精度只有约 15 位十进制有效数字,可能出现判断偏差。不过这道题的 n2 最大不超过 33² + 68 = 1157,完全在安全范围内。循环上限直接写死成33,是因为前面推导已经证明 m ≤ 33。如果你把 diff 换成其他值,上限也要重新推导,不能照抄。输出结果不变,仍然是 x = 156。4.3 三种解法对比用表格直观对比一下:解法枚举对象循环次数时间复杂度优点缺点暴力枚举x约1170次O(N)思路直接,最贴合题目字面需要事先确定范围,否则容易瞎猜上界因数分解68的因数不到10次O(√D)数学思路最漂亮,不遗漏解需要掌握同奇偶筛选逻辑倒推枚举m34次O(√D)代码短,判断逻辑直观需要先推导 m 的上界三种方法各有定位:新手先写暴力法理解题意,面试时写因数分解法展示思维层次,工程上做通用工具时用倒推枚举法最不容易出错。提示:如果你要把解法做成一个通用的“平方差等于 D 的整数对查找器”,因数分解法是最稳的,因为它的循环次数只跟 D 的因数个数有关,并且天然不会漏解。5. 实战中的坑与常见问题速查5.1 完全平方数判断的精度问题这是我在实际代码评审里看到最多的问题。很多人写判断正方形是这样写的:double sqrt = Math.sqrt(num); if (sqrt == (int) sqrt) { return true; }这段代码在小整数范围内能用,但有两个隐患:(int) sqrt是把 double 强转成 int,如果 sqrt 非常接近一个整数但比整数小一点点,强转会得到比实际整数小1的数,导致误判。double 在表达极大整数时精度不足。当 num 超过 2^53 时,甚至 num 本身转成 double 就会丢精度。稳妥写法我之前已经给过:先算 int sqrt = (int) Math.sqrt(num),再判断 sqrt * sqrt == num。或者干脆用 BigDecimal 做完全限定判断,不过那道题用不上。5.2 整数溢出风险这道题的中间变量虽然不大,但稍微改版就可能撞上溢出。比如改成“加上1000000后是平方数,加上2000000后也是平方数”,m 和 x 都会大不少,int 乘法很容易出问题。我的习惯是:所有可能先乘后判的中间量都声明成 long。Java 里 long 的范围比 int 大太多,在小题目里基本等于“免费保险”。如果真的遇到更大范围,那就要考虑用java.math.BigInteger了,不过那是另一个场景的事。5.3 负数范围与零的处理很多初学者忽略 x 可以是负数。这道题里 x = -168 就是一个边界值,因为 x + 168 = 0 = 0²,但 x + 100 = -68,不是平方数。把所有边界情况都考虑一遍,才算完整解题。还有 m、n 取负数的问题。m = -16 时,m² 同样是256,不影响 x。所以因数分解法里我默认 m、n 取非负整数,最终 x 的结果不变。如果你想跟面试官讨论这个点,可以主动提一句:因为平方的符号不影响结果,所以非负解已经覆盖所有情况。5.4 面试里怎么答才算加分这道题在很多面试里不是笔试题,而是口述题。面试官可能一边喝茶一边问你:这个数是多少?我建议的回答节奏是:先说出数学关系:设 x + 100 = m²,x + 168 = n²。然后相减得到 n² - m² = 68。接着因式分解:68 = 2 × 34,所以 n - m = 2,n + m = 34。解出 n = 18,m = 16,x = 156。最后验证:156 + 100 = 256,156 + 168 = 324。整个过程大概一分钟,不用写一行代码。如果你一上来就说“我循环跑一下”,面试官心里的评分至少降一档。这道题考察的核心不是你会不会用 Java,而是你有没有把问题数学化的意识。6. 个人经验与扩展建议这道题我几乎每年都会用不同的语言重写一遍,每次都会有新的感受。最早我写 Python 版本的时候,用的就是暴力循环,上界拍脑袋写了十万,结果跑完还怀疑是不是漏了解。后来做了数学推导,才发现上界一千都不到。那种“豁然开朗”的感觉让我养成了一个习惯:拿到算法题,先别碰键盘,用纸笔把约束条件理清楚,尤其是变量之间的数量关系。如果你打算把这类题整理成一套通用工具,可以沿这个方向扩展:输入一个差值 D 和基准偏移量 offset,输出所有满足 (x + offset) 与 (x + offset + D) 均为完全平方数的整数 x。核心思路就是因数分解 + 同奇偶筛选。我之前写过一个这样的方法,核心代码只有十几行,却能处理一整个系列的“平方差”题目,效果很香。另外,我想把这道题再往深处挖一点:当你把 D 固定后,x 的解其实很有限,因为 n - m ≥ 1 决定了 m 是有限集。遇到这类“两条件都是平方数”的问题,都可以先转换成平方差,再用因数分解收束范围。这个方法不局限于 Java,C、Python、JavaScript 都适用。最后再分享一个小经验:写完代码后,不要只看输出结果就收工。手动把等式代回去验证一遍,是成本最低的防错手段。我见过有同学因为 m、n 的符号问题,答案输出了一堆重复项,就是因为缺了最后的数学验证。这道题里的 156 虽然简单,但验证步骤传递的习惯,在以后处理更复杂的数学编程题时,价值会成倍放大。
看到这类题目,很多人的第一反应是掏出IDE准备暴力循环,但真正动手之后会发现两个问题:一是循环上界设多少完全没谱,二是就算跑出结果也不敢保证唯一。这道“整数加100和加168都是完全平方数”的经典题,其实藏着非常漂亮的数学结构,用Java实现时也有不少值得讲的细节。这篇文章我会从题目拆解开始,给出三种可落地的Java解法,再把判断完全平方数时容易踩的浮点精度坑、溢出坑都摊开讲清楚。1. 先把题目拆透:这道题到底在问什么1.1 变量与条件的数学翻译题目原文是:一个整数,它加上100后是一个完全平方数,加上168又是一个完全平方数,请问该数是多少?先别急着写代码,把自然语言翻译成数学语言。设这个整数为 x,那么存在两个整数 m 和 n,使得:x + 100 = m²x + 168 = n²这里有一个很关键的顺序问题:两个条件是“分别加上100和168”,不是“加上100之后,再在结果上加上168”。如果你把第二个条件理解成 x + 100 + 168 = n²,那整道题的答案会完全不一样,后面我会专门讲这个表述陷阱。既然是同一个 x,那么两个式子相减是最自然的操作:(x + 168) - (x + 100) = n² - m²化简后就是:n² - m² = 68到这里题目已经从“找一个整数 x”转化成了“找两个整数 m、n,使它们的平方差等于68”,然后 x = m² - 100 自然就出来了。这一步转化是整个题目的灵魂,也是面试官最想看到的思维过程。1.2 为什么“68”这么关键68这个数字不是随便来的,它就是两个条件之间的差的绝对值。168 - 100 = 68,就这么简单。但恰恰是这个差值决定了题目的解法和解的个数。平方差公式告诉我们:n² - m² = (n - m)(n + m)所以问题又变成了:把68拆成两个整数相乘,并且这两个因数恰好等于 n - m 和 n + m。这就把一个看似无限的枚举问题,变成了一个有限范围内的因数分解问题。68的因数对不多,也就几组,手算都能算出来,编程做更是秒杀。1.3 先别急着写代码:手推一遍答案在打开编辑器之前,我建议你先拿纸笔推一遍。这个过程会让你对解法有真正的理解,而不是背代码。68的因数对包括:1 × 682 × 344 × 17注意,n - m 和 n + m 必须同奇偶,因为它们的和是 2n,一定是偶数;它们的差是 2m,也一定是偶数。所以如果两个因数一个是奇数一个是偶数,直接排除。看一遍:1 和 68,一奇一偶,排除。2 和 34,两个都是偶数,可行。于是 n - m = 2,n + m = 34,解得 n = 18,m = 16。4 和 17,一奇一偶,排除。所以 m = 16,n = 18,代回 x = m² - 100 = 256 - 100 = 156。验证一下:156 + 100 = 256 = 16²,156 + 168 = 324 = 18²。答案完美。注意:这里用“因数对同奇偶”筛选有一个前提,就是 m、n 我们默认取非负整数。实际上 m 和 n 取负数也不影响平方结果,但因为平方值只看绝对值,最终得到的 x 不变。这个细节在面试中主动提出来,会显得你考虑问题很全面。2. Java解法一:暴力枚举,简单但够用2.1 思路与边界确定最直白的思路是:把 x 的可能取值一个个试过去,每个 x 都检查 x + 100 和 x + 168 是不是完全平方数。问题来了:x 的枚举范围怎么定?下界很明确,因为 x + 168 必须是非负数(完全平方数最小是0),所以 x ≥ -168。上界呢?这是暴力法最尴尬的地方。题目没有限制 x 的范围,理论上它可以非常大。但因为 n² - m² = 68,如果 n > m,那么 n - m ≥ 1(整数之间最小差1),于是:n² - m² = (n - m)(n + m) ≥ 1 × (2m + 1) = 2m + 1也就是说 2m + 1 ≤ 68,解得 m ≤ 33。这其实告诉我们,就算用暴力法,也没必要把 x 枚举到天上去。x = m² - 100,m 最大33,所以 x 最大不超过 33² - 100 = 989。看到没有,上界其实小得惊人。当然,如果你不想在暴力法里做这个推导,也可以拍脑袋给一个比较大的上界,比如十万、一百万。但知道了数学关系之后,你会更安心:枚举到 1000 以内就够了。2.2 基础版代码实现直接上代码:public class BruteForceSolution { public static void main(String[] args) { int count = 0; // 根据 m <= 33 可知 x 最大为 33*33 - 100 = 989 for (int x = -168; x <= 1000; x++) { if (isPerfectSquare(x + 100) && isPerfectSquare(x + 168)) { System.out.println("找到整数: " + x); count++; } } System.out.println("共找到 " + count + " 个"); } private static boolean isPerfectSquare(int num) { if (num < 0) { return false; } int sqrt = (int) Math.sqrt(num); return sqrt * sqrt == num; } }isPerfectSquare的写法是这套代码的核心:先用Math.sqrt开方,再强转成 int 截断小数部分,最后用乘法回验。比如 num = 256,sqrt = 16.0,转 int 还是16,16×16 = 256,相等,返回 true。比如 num = 257,sqrt ≈ 16.03,转 int 变成16,16×16 = 256 ≠ 257,返回 false。输出结果:找到整数: 156 共找到 1 个这个版本代码量最小,逻辑直白,特别适合第一次接触这道题的人。2.3 这版代码的坑:Math.sqrt的浮点隐患暴力解法看着简单,但Math.sqrt返回的是 double,这里藏着几个细节。第一个坑:用Math.sqrt(num)后强转 int,如果 num 本身是负数,Math.sqrt会返回 NaN,强转成 int 的结果在 Java 里是 0。所以isPerfectSquare里必须先做if (num < 0)的防御判断,否则你的逻辑会出错。我见过有人写出(int)Math.sqrt(-1) == 0,然后 0×0 == 0,误判 -1 是完全平方数,这就是典型的防御性编程没做到位。第二个坑:double 的精度问题。当 num 非常大时,Math.sqrt的结果可能是一个很接近整数的浮点数,比如 999999.9999999,强转 int 后变成 999999,回验就可能不相等。更稳妥的做法是判断:double s = Math.sqrt(num); if (s - Math.floor(s) < 1e-10) { // 认为是完全平方数 }但我不推荐这个写法,因为误差阈值难定,不同数量级下表现不稳定。最可靠的永远是“整数回验”方案:转成 int 后乘回来再比较原数。只要 num 不超过 int 范围,这个方案就是精确的。第三个坑:乘法溢出。sqrt * sqrt如果 num 接近 Integer.MAX_VALUE,sqrt 约等于 46340,46340 × 46340 = 2147395600,没溢出。但如果 num 超过 int 范围,或者你把判断逻辑用在了 long 类型上,就可能悄悄溢出变成负数,导致判断结果完全错误。下面一节会专门讲大数场景。3. Java解法二:因数分解,一步到位3.1 从平方差公式到因数对暴力法适合理解题目,但如果你想在面试里展示出数学功底,因数分解法才是真正的标准答案。前面已经推过:n² - m² = (n - m)(n + m) = 68。设 a = n - m,b = n + m,那么 a × b = 68,并且有 b > a > 0(因为 m、n 非负且 n > m,这里先讨论正数情况)。同时,a 和 b 必须同奇偶。理由很简单:a + b = 2n,是偶数,所以 a、b 要么都是偶数,要么都是奇数。而 a × b = 68 是偶数,排除了“两个奇数相乘”的可能,所以 a 和 b 必须都是偶数。这就是一个非常规整的逻辑链:枚举 68 的所有正因数对。过滤掉不同奇偶的组合。过滤掉 a >= b 的组合(因为 n - m < n + m)。解出 n = (a + b) / 2,m = (b - a) / 2。代回 x = m² - 100。3.2 奇偶性筛选的数学理由这一步值得单独说一下。为什么网上很多答案里,看到 68 = 2 × 34 就直接跳过了 1 × 68 和 4 × 17?有人说是“因为 68 的因数里只有 2 和 34 都是偶数”,这其实只是结果不是原因。真正的原因就是我上面写的同奇偶判断。把它讲明白,你会比那些“背答案”的人强很多:如果 a、b 一奇一偶,那么 a + b 是奇数,但 a + b = 2n 必须是偶数,矛盾。如果 a、b 都是奇数,那么 a × b 是奇数,但 a × b = 68 是偶数,矛盾。所以 a、b 只能都是偶数。有了这层推导,你甚至可以给出一个一般性结论:当目标差值 D 为偶数时,如果 D 不是 4 的倍数,那么 D 无法分解成两个偶数的乘积,平方差为 D 的整数对就不存在。比如 D = 2,只能拆成 1×2,一奇一偶,无解。这个扩展思路在后面讲通用解法时非常有用。3.3 Java实现:枚举因数并筛选我把因数分解法的完整代码写出来:public class FactorizationSolution { public static void main(String[] args) { int diff = 168 - 100; // 68 for (int a = 1; a * a <= diff; a++) { if (diff % a != 0) { continue; } int b = diff / a; // 要求 b > a,且 a、b 同奇偶 if (b <= a) { continue; } if ((a & 1) != (b & 1)) { continue; } int m = (b - a) / 2; int n = (a + b) / 2; int x = m * m - 100; System.out.println("a=" + a + ", b=" + b + ", m=" + m + ", n=" + n + ", x=" + x); // 验证 int s1 = m * m; int s2 = n * n; if (s1 == x + 100 && s2 == x + 168) { System.out.println("验证通过"); } } } }这段代码里有个小技巧:循环条件a * a <= diff,意味着 a 只需要枚举到 sqrt(diff) 就够了,因为如果 a 是较小因数,b = diff / a 自动就是较大因数。你不需要枚举到 68,只要 a 从1试到8(因为 8 × 8 = 64 ≤ 68),四组因数对都能覆盖到。运行输出:a=2, b=34, m=16, n=18, x=156 验证通过代码只输出一组结果,数学上也是唯一解,跟手推完全一致。3.4 一个重要扩展:一个数加上100是平方数,再加上168也是平方数我前面提醒过表述陷阱,现在用因数分解法来展示这个陷阱的威力。如果题目改成:“一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数”,那条件就变成:x + 100 = m²x + 100 + 168 = n²两个式子相减得到:n² - m² = 168而不是 68。用同样的因数分解代码,把 diff 改成168:168的因数对(a < b)有:1 × 168,一奇一偶,排除。2 × 84,两个偶数,可行,m = 41,n = 43,x = 41² - 100 = 1581。3 × 56,一奇一偶,排除。4 × 42,两个偶数,可行,m = 19,n = 23,x = 19² - 100 = 261。6 × 28,两个偶数,可行,m = 11,n = 17,x = 21。7 × 24,一奇一偶,排除。8 × 21,一奇一偶,排除。12 × 14,两个偶数,可行,m = 1,n = 13,x = -99。所以这个“改版”题目有四个答案:-99、21、261、1581。你看,仅仅是把“加上168”换成“再加上168”,差值就从68变成168,解的个数也从1个变成4个。这就是为什么读题时一字之差,写代码前务必把数学关系确认清楚。以后不管遇到哪个版本,用因数分解法都能快速解出,还不会漏解。4. Java解法三:倒推双循环,更通用的视角4.1 思路:换个角度枚举暴力法是枚举 x,因数分解法是拆因数,还有第三种思路:直接枚举 m,然后用 n² = m² + 68 去判断。为什么要枚举 m?因为 m 是平方根,它的取值范围比 x 小得多。根据之前的推导,n > m 时:n² - m² = (n - m)(n + m)由于 n - m ≥ 1,n + m ≥ 2m + 1,所以:68 ≥ 2m + 1m 最大只能取到 33。也就是说,只需要从 0 到 33 枚举 m,34次判断就能解决问题。4.2 代码实现与复杂度分析代码长这样:public class EnumerateRootSolution { public static void main(String[] args) { int diff = 168 - 100; for (int m = 0; m <= 33; m++) { long m2 = (long) m * m; // 避免 int 乘法溢出 long n2 = m2 + diff; long n = (long) Math.sqrt(n2); if (n * n == n2) { long x = m2 - 100; System.out.println("m=" + m + ", n=" + n + ", x=" + x); } } } }我来解释几个细节:(long) m * m这一步很关键。如果 m 是 int,m * m 先按 int 计算,一旦超过 2^31 - 1 就会溢出,结果变成负数。虽然这里 m 最大33不会溢出,但培养这种防御习惯很重要,尤其是日后处理大数题目。Math.sqrt(n2)的参数是 long,会自动转成 double。如果 n2 很大,double 精度只有约 15 位十进制有效数字,可能出现判断偏差。不过这道题的 n2 最大不超过 33² + 68 = 1157,完全在安全范围内。循环上限直接写死成33,是因为前面推导已经证明 m ≤ 33。如果你把 diff 换成其他值,上限也要重新推导,不能照抄。输出结果不变,仍然是 x = 156。4.3 三种解法对比用表格直观对比一下:解法枚举对象循环次数时间复杂度优点缺点暴力枚举x约1170次O(N)思路直接,最贴合题目字面需要事先确定范围,否则容易瞎猜上界因数分解68的因数不到10次O(√D)数学思路最漂亮,不遗漏解需要掌握同奇偶筛选逻辑倒推枚举m34次O(√D)代码短,判断逻辑直观需要先推导 m 的上界三种方法各有定位:新手先写暴力法理解题意,面试时写因数分解法展示思维层次,工程上做通用工具时用倒推枚举法最不容易出错。提示:如果你要把解法做成一个通用的“平方差等于 D 的整数对查找器”,因数分解法是最稳的,因为它的循环次数只跟 D 的因数个数有关,并且天然不会漏解。5. 实战中的坑与常见问题速查5.1 完全平方数判断的精度问题这是我在实际代码评审里看到最多的问题。很多人写判断正方形是这样写的:double sqrt = Math.sqrt(num); if (sqrt == (int) sqrt) { return true; }这段代码在小整数范围内能用,但有两个隐患:(int) sqrt是把 double 强转成 int,如果 sqrt 非常接近一个整数但比整数小一点点,强转会得到比实际整数小1的数,导致误判。double 在表达极大整数时精度不足。当 num 超过 2^53 时,甚至 num 本身转成 double 就会丢精度。稳妥写法我之前已经给过:先算 int sqrt = (int) Math.sqrt(num),再判断 sqrt * sqrt == num。或者干脆用 BigDecimal 做完全限定判断,不过那道题用不上。5.2 整数溢出风险这道题的中间变量虽然不大,但稍微改版就可能撞上溢出。比如改成“加上1000000后是平方数,加上2000000后也是平方数”,m 和 x 都会大不少,int 乘法很容易出问题。我的习惯是:所有可能先乘后判的中间量都声明成 long。Java 里 long 的范围比 int 大太多,在小题目里基本等于“免费保险”。如果真的遇到更大范围,那就要考虑用java.math.BigInteger了,不过那是另一个场景的事。5.3 负数范围与零的处理很多初学者忽略 x 可以是负数。这道题里 x = -168 就是一个边界值,因为 x + 168 = 0 = 0²,但 x + 100 = -68,不是平方数。把所有边界情况都考虑一遍,才算完整解题。还有 m、n 取负数的问题。m = -16 时,m² 同样是256,不影响 x。所以因数分解法里我默认 m、n 取非负整数,最终 x 的结果不变。如果你想跟面试官讨论这个点,可以主动提一句:因为平方的符号不影响结果,所以非负解已经覆盖所有情况。5.4 面试里怎么答才算加分这道题在很多面试里不是笔试题,而是口述题。面试官可能一边喝茶一边问你:这个数是多少?我建议的回答节奏是:先说出数学关系:设 x + 100 = m²,x + 168 = n²。然后相减得到 n² - m² = 68。接着因式分解:68 = 2 × 34,所以 n - m = 2,n + m = 34。解出 n = 18,m = 16,x = 156。最后验证:156 + 100 = 256,156 + 168 = 324。整个过程大概一分钟,不用写一行代码。如果你一上来就说“我循环跑一下”,面试官心里的评分至少降一档。这道题考察的核心不是你会不会用 Java,而是你有没有把问题数学化的意识。6. 个人经验与扩展建议这道题我几乎每年都会用不同的语言重写一遍,每次都会有新的感受。最早我写 Python 版本的时候,用的就是暴力循环,上界拍脑袋写了十万,结果跑完还怀疑是不是漏了解。后来做了数学推导,才发现上界一千都不到。那种“豁然开朗”的感觉让我养成了一个习惯:拿到算法题,先别碰键盘,用纸笔把约束条件理清楚,尤其是变量之间的数量关系。如果你打算把这类题整理成一套通用工具,可以沿这个方向扩展:输入一个差值 D 和基准偏移量 offset,输出所有满足 (x + offset) 与 (x + offset + D) 均为完全平方数的整数 x。核心思路就是因数分解 + 同奇偶筛选。我之前写过一个这样的方法,核心代码只有十几行,却能处理一整个系列的“平方差”题目,效果很香。另外,我想把这道题再往深处挖一点:当你把 D 固定后,x 的解其实很有限,因为 n - m ≥ 1 决定了 m 是有限集。遇到这类“两条件都是平方数”的问题,都可以先转换成平方差,再用因数分解收束范围。这个方法不局限于 Java,C、Python、JavaScript 都适用。最后再分享一个小经验:写完代码后,不要只看输出结果就收工。手动把等式代回去验证一遍,是成本最低的防错手段。我见过有同学因为 m、n 的符号问题,答案输出了一堆重复项,就是因为缺了最后的数学验证。这道题里的 156 虽然简单,但验证步骤传递的习惯,在以后处理更复杂的数学编程题时,价值会成倍放大。