☰
Java求解完全平方数整数题:数学推导与代码实现
2026/10/21 7:18:57 网站建设 项目流程

最近有同学拿着一道经典入门题来问我:一个整数,它加上100后是一个完全平方数,加上168又是一个完全平方数,这个数是多少?题目本身不难,但用Java去写的时候,我发现很多人要么循环边界拍脑袋,要么把题意理解偏了,最后答案对不上。这篇文章就把这道题从头到尾拆开讲一遍,覆盖数学推导、Java代码实现、常见坑和通用解法,适合刚学Java基础、准备机试或者复习面试算法题的人。

先说结论:按题目字面理解,答案是156。后面我会具体验证。但如果你在网上搜这道题,经常会看到-99、21、261、1581这组答案,那其实是另一个版本,也就是“加上100后是完全平方数,再加上168又是一个完全平方数”,我后面会专门解释这个差异。无论你遇到哪个版本,这篇文章里的方法和代码都能直接套。

1. 先做数学题,再写Java代码

1.1 把题目“翻译”成两个方程

很多初学者拿到这道题,第一反应是开一个循环,从-100000到100000,一个个试过去。这种思路不能算错,但很浪费,而且边界到底设多少,心里完全没底。

正确做法是先设未知数。题目说“一个整数”,我们把它记为x。题目字面意思是:

  • x + 100 是一个完全平方数
  • x + 168 也是一个完全平方数

完全平方数就是一个整数的平方,比如0、1、4、9、16这些都是。注意0也算完全平方数,因为0可以写成0的平方。负数不是完全平方数,因为没有任何整数平方之后是负数。

于是可以设:

x + 100 = m * m x + 168 = n * n

其中m、n都是整数。这里m和n到底取正取负都不影响最终结果,因为负数的平方和正数的平方一样,所以后面统一按非负整数来讨论。

很多代码写不出来,卡就卡在没把题目变成方程。一旦方程列出来,问题就从“猜一个数”变成了“解一个整数方程”,思路完全不一样。

1.2 两个方程相减,得到一个关键等式

把上面两个等式相减:

(x + 168) - (x + 100) = n * n - m * m

左边等于68,右边等于(n - m)(n + m)。这就是这道题最核心的突破点:

(n - m)(n + m) = 68

这是一个特别好的形式。因为n和m都是整数,所以(n - m)和(n + m)也都是整数。两个整数的乘积等于68,可能性是有限的。我们不需要去无穷无尽地遍历x,只需要看68能拆成哪两个整数相乘。

这就是“先数学建模再编码”的意义。直接写循环当然也能碰出答案,但你很难跟别人解释为什么循环边界是100000而不是1000000,也无法保证不漏解。但因数分解这条路,每一步都有严格的数学依据。

1.3 为什么答案不是无穷多个

有个很自然的疑问:完全平方数有无限多个,x难道不会有很多很多个吗?事实上不会。

看这个式子:

n * n - m * m = 68

两个平方数的差是固定的68。平方数之间的差距会随着数字变大而越来越大,比如100到121差21,10000到10201差201。所以固定差68的情况下,n和m不可能无限增大,甚至范围很小。

具体来说,因为n > m,所以(n - m)至少是1。又因为(n - m)(n + m) = 68,所以(n + m)最大只能是68。这直接推出n不超过34,m不超过33。也就是说,这道题的搜索空间其实非常小,就算暴力枚举,m只要从0试到34就绝对够了,根本不用到100000。

这个边界不是拍脑袋拍出来的,是从方程本身推出来的。后面写Java代码的时候,这个边界就是我们的安全网。

2. 手工推导:到底有哪些解

2.1 枚举68的因数对

68的正因数有:1、2、4、17、34、68。我们把它拆成两个因数相乘,同时要考虑(n - m)和(n + m)的奇偶性。

因为:

(n - m) + (n + m) = 2n

两个数相加等于2n,说明(n - m)和(n + m)必须同奇偶。如果一奇一偶,n就不是整数。

逐个看:

  • 1和68:1是奇数,68是偶数,奇偶性不同,舍去
  • 2和34:2和34都是偶数,n = (2 + 34) / 2 = 18,m = (34 - 2) / 2 = 16,可行
  • 4和17:4是偶数,17是奇数,奇偶性不同,舍去

所以按题面x + 100和x + 168理解,唯一可行的是m = 16,n = 18。于是:

x = m * m - 100 = 16 * 16 - 100 = 256 - 100 = 156

验证一下:

156 + 100 = 256 = 16 * 16 156 + 168 = 324 = 18 * 18

完全符合。所以答案就是156。

这里还要提一句:如果允许m和n取负数,比如m = -16、n = -18,那么m²和n²不变,算出来的x还是156,不会多出新的解。所以这道题不需要考虑负数解的情况。

2.2 网上的四个答案到底怎么来的

很多人搜这道题,会搜到-99、21、261、1581这组答案。他们用我的代码跑却只输出156,就会很困惑。问题出在题目版本上。

流传更广的版本是这样的:“一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数”。注意这里有“再加上”三个字,意思是:

x + 100 = m * m x + 100 + 168 = n * n

也就是x加100之后,再额外加168,总共加了268才是第二个完全平方数。这种情况下:

n * n - m * m = 168

枚举168的因数对,并且要求两个因数同奇偶:

n - mn + mmnx
28441431581
4421923261
628111721
1214113-99

逐个验证:

  • -99 + 100 = 1 = 1²,-99 + 268 = 169 = 13²
  • 21 + 100 = 121 = 11²,21 + 268 = 289 = 17²
  • 261 + 100 = 361 = 19²,261 + 268 = 529 = 23²
  • 1581 + 100 = 1681 = 41²,1581 + 268 = 1849 = 43²

所以网上常见的四个答案,对应的是“再加上168”的版本。你现在按题面“加上168又是一个完全平方数”理解,正确答案是156。

这也算一个提醒:遇到这种流传很广的题,先确认题目原意,再动手写代码,不然后面全白费。

2.3 把推导变成可复用的结论

如果题目变成“x加a是完全平方数,x加b也是完全平方数”,其中b > a,设:

x + a = m * m x + b = n * n

那么:

b - a = n * n - m * m = (n - m)(n + m)

解法完全一样:把d = b - a分解成两个同奇偶的因数,然后算出m和n,最后反推x。

这就是这道题最有价值的地方。它不是一个一次性的脑筋急转弯,而是一整套可以复用的数学模板。任何“两个平方数之差为固定值”的题目,都可以用这套思路秒解。

3. Java代码实现:三种写法,从笨到优雅

3.1 写法一:无脑双重循环,暴露问题

先看很多新手会写的版本:

public class BruteForce { public static void main(String[] args) { for (long m = 0; m <= 100000; m++) { for (long n = m + 1; n <= 100000; n++) { if (n * n - m * m == 68) { long x = m * m - 100; System.out.println("x = " + x); } } } } }

这段代码在某些情况下确实能输出156,但它有三个问题。

第一,100000这个边界是拍脑袋定的。虽然这道题实际只需要m到16就能找到解,但如果你不知道数学推导,你怎么证明100000够用?万一下一题不是68,可能就需要更大的边界。

第二,效率差。m和n各10万,理论上要执行100亿次,根本跑不完。这道题因为k=68限制了实际有效范围,但程序本身不知道这一点,它还是会傻乎乎地遍历。

第三,m和n的平方运算可能溢出。如果用int而不是long,m到100000时m²已经是100亿,超出int范围。用long只是延缓问题,不是解决根因。

所以这种写法只能用于理解“枚举”这件事本身,不适合作为最终答案。

3.2 写法二:单层枚举加平方数判断

既然我们已经知道m最多只有34,就完全没有必要双重循环。枚举m,然后用一个辅助方法判断n²是不是完全平方数,代码清爽很多:

public class SingleLoop { public static void main(String[] args) { for (long m = 0; m <= 34; m++) { long x = m * m - 100; long n2 = x + 168; if (isPerfectSquare(n2)) { long n = (long) Math.sqrt(n2); System.out.println("x = " + x + ", m = " + m + ", n = " + n); } } } private static boolean isPerfectSquare(long n) { if (n < 0) { return false; } long t = (long) Math.sqrt(n); if (t * t == n) { return true; } t = t + 1; return t * t == n; } }

这里有个细节要注意。Math.sqrt返回的是double,直接用强转long可能会因为精度问题导致向下取整偏差。所以判断一个数是不是完全平方数,不要直接用Math.sqrt(n)和某个数相等,而是先算出平方根整数t,再比较t * t和n。为了更稳,我还会多检查一下t + 1的平方。

跑一下输出:

x = 156, m = 16, n = 18

这个版本只有35次循环,几乎是瞬时完成。而且边界34是数学推导出来的,不是猜的。

3.3 写法三:因数分解通用解法

如果追求最优雅的解法,可以直接对差值做因数分解。下面这个方法把差值作为参数传进去,将来换任何两个数都能用:

import java.util.ArrayList; import java.util.List; public class FactorSolver { public static void main(String[] args) { System.out.println("题面版本,差值68:"); solve(68); System.out.println(); System.out.println("网上版本,差值168:"); solve(168); } private static void solve(long diff) { List<Long> solutions = new ArrayList<>(); for (long d = 1; d <= diff / d; d++) { if (diff % d != 0) { continue; } long e = diff / d; if (((d + e) & 1) == 1) { continue; } long m = (e - d) / 2; long n = (e + d) / 2; long x = m * m - 100; solutions.add(x); System.out.println("m = " + m + ", n = " + n + ", x = " + x); } if (solutions.isEmpty()) { System.out.println("无解"); } } }

这套代码的逻辑是:

  • d取较小因子,e取较大因子
  • d = n - m,e = n + m
  • 判断d和e同奇偶,也就是d + e是偶数
  • 用公式算出m和n,再算出x

输出结果:

题面版本,差值68: m = 16, n = 18, x = 156 网上版本,差值168: m = 41, n = 43, x = 1581 m = 19, n = 23, x = 261 m = 11, n = 17, x = 21 m = 1, n = 13, x = -99

3.4 三种写法怎么选

写法思路循环次数是否依赖数学推导适用场景
双重循环无脑枚举直接判断两平方数之差极大否仅用于理解枚举思想
单层枚举m枚举较小底数,再判平方35次是实际做题推荐
因数分解通用解直接分解差值sqrt(diff)次是面试展示、扩展复用

如果是笔试时间里,我会写第二种,因为代码直观,别人也容易看懂。如果是面试聊思路,我会先讲因数分解,再说可以用单层循环验证。第一种就当作反面教材,告诉面试官你知道这样写的问题在哪。

4. 运行结果、常见坑与通用工具函数

4.1 运行结果验证

用单层写法跑出来的结果是:

x = 156, m = 16, n = 18

手动验证:

156 + 100 = 256 = 16 * 16 156 + 168 = 324 = 18 * 18

完全符合试题要求。

我还建议你写程序时不要只依赖程序输出,先手工推导一遍,然后用程序来验证推导。这种“手算和机算互相验证”的习惯,在做算法题时特别重要。很多bug,你一对照手工结果就能发现。

4.2 坑一:完全平方数判断写成浮点取模

新手最容易写出的错误判断:

if (Math.sqrt(n) % 1 == 0) { // 浮点取模判断整数,不靠谱 }

或者:

if (Math.sqrt(n) == (int) Math.sqrt(n)) { // 强转截断后再和浮点数比较,会有精度问题 }

这两种写法在大多数情况下凑巧能用,但在某些边界数字上会出现误判。正确做法是:

long t = (long) Math.sqrt(n); if (t * t == n) { // 是完全平方数 }

先把根号结果转成long,再比较平方是否相等,全程用整数运算,避免浮点精度干扰。

4.3 坑二:int溢出和循环边界拍脑袋

如果一不小心用int:

for (int mm = 0; mm <= 100000; mm++) { int n2 = mm * mm + 68; // mm * mm 到100000时已经溢出 }

int最大也就21亿多,mm到46341时mm * mm就会超过int上限。这道题实际只需要mm到16,所以int在本题不会爆,但如果你把循环放大到十万、百万,或者把题目参数改大,int一定爆。

循环边界也是一个问题。很多人写100000、1000000,纯粹是“觉得够大了”。但下一道题可能就不是68这个差值,而是别的数。正确做法是先用因数分解确定边界,再写循环。数学推导的边界才是可靠的。

4.4 坑三:题面歧义——加上168和再加上168

这道题最大的隐藏坑其实是题意理解差异。

按“x + 100是完全平方数,x + 168也是完全平方数”理解,答案是156。

按“x + 100是完全平方数,x + 100 + 168也是完全平方数”理解,答案是-99、21、261、1581。

所以当你看到网上答案跟你跑出来的不一样时,先别急着怀疑代码。回到题目本身,看它到底有没有“再加上”三个字。这个坑之所以经典,是因为它不在代码层面,而在需求理解层面。

放到真实开发里,这就相当于产品需求描述有歧义。你写了半天,最后发现是你理解错了。所以我在开头才说,这道题虽然简单,但很有代表性。

4.5 一个更稳健的isPerfectSquare工具函数

对于普通long范围内的数,t = (long) Math.sqrt(n)然后比较t * t,基本够用。但如果你处理的数非常大,比如接近Long.MAX_VALUE,Math.sqrt的double精度可能不够,强转后再平方比较也可能出错。

Java 9及以上可以用BigInteger的标准方法:

import java.math.BigInteger; public class PerfectSquareUtil { public static boolean isPerfectSquare(long n) { if (n < 0) { return false; } BigInteger value = BigInteger.valueOf(n); BigInteger sqrt = value.sqrt(); return sqrt.multiply(sqrt).equals(value); } }

BigInteger.sqrt()是标准库实现,对于任意大小的整数都能给出精确的整数平方根。如果你面试时被问到“超大数的完全平方数判断”,直接答BigInteger.sqrt(),这是一个加分点。

如果环境是Java 8,没有BigInteger.sqrt(),可以自己写牛顿迭代求整数平方根,但一般面试题用不到,这里就不展开了。

5. 一个延伸问题:如果差值变了,解法照样秒杀

5.1 把题目参数化:加100和加200

这套方法最大的好处是通用。假设题目改成:

“一个整数,它加上100后是一个完全平方数,加上200又是一个完全平方数,请问该数是多少?”

设:

x + 100 = m * m x + 200 = n * n

相减得到:

n * n - m * m = 100

分解100的因数对,并且要求同奇偶:

  • (2, 50):m = (50 - 2) / 2 = 24,n = (50 + 2) / 2 = 26,x = 24 * 24 - 100 = 476
  • (4, 25):奇偶性不同,舍去
  • (10, 10):m = 0,n = 10,x = 0 * 0 - 100 = -100

所以有两个解:-100和476。

验证一下:

-100 + 100 = 0 = 0² -100 + 200 = 100 = 10² 476 + 100 = 576 = 24² 476 + 200 = 676 = 26²

注意这里出现了m = 0的情况,也就是说0这个完全平方数开始影响结果了。如果题目限定“正整数”,那-100要排除掉;如果只说“整数”,-100就是合法解。

这就是为什么我在开头提到0算完全平方数很重要,在一些参数下,它会直接决定解集合的个数。

5.2 如果diff = 2,为什么无解

再举个反直觉的例子:是否存在整数x,让它加100和加102都是完全平方数?

设:

x + 100 = m * m x + 102 = n * n

相减得到:

n * n - m * m = 2

70的乘积形式:

(n - m)(n + m) = 2

2只能拆成1和2,而1 + 2 = 3是奇数,n不是整数。所以不存在这样的整数x。

这个结论用枚举也能验证,但枚举到什么时候是个头?用因数分解,一步就证明无解。这就是数学推导的价值。

5.3 从这道题能带走什么

我见过太多学习者卡在这种题上,不是因为不会Java,而是因为拿到题目直接开写代码,跳过了“列方程、化简、确定边界”这三步。这道题给我的启发只有一个,但非常管用:遇到数值搜索题,先动手算几个数,再决定用多少行代码。

你甚至可以把这个因数分解方法封装成一个工具方法,以后遇到“某个数加a和加b都是完全平方数”这类问题,直接传入差值,秒出所有解。这道题不是终点,它是一个可以复制到很多场景的起点。

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

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

立即咨询