文章目录
题目链接
- 原题链接:https://leetcode.cn/problems/perfect-squares/description/?envType=study-plan-v2&envId=top-100-liked
题目说明
给你一个整数 n,返回 和为 n 的完全平方数的最少数量。
- 完全平方数:形如
1, 4, 9, 16, ...(即某个整数的平方) - 示例:
n = 12,答案是3(12 = 4 + 4 + 4)n = 13,答案是2(13 = 4 + 9)
约束(LeetCode):
1 <= n <= 10^4
解题思路总览
解法一:动态规划(推荐,通用且好理解)
原理
定义 dp[i]:表示组成整数 i 所需的最少完全平方数个数。
那么:
d p [ i ] = min j 2 ≤ i ( d p [ i − j 2 ] + 1 ) dp[i] = \min_{j^2 \le i}(dp[i-j^2] + 1) dp[i]=j2≤imin(dp[i−j2]+1)
含义是:最后一次选了一个平方数 j*j,前面剩下 i-j*j 的最优解再加 1。
状态转移流程图
Java 代码
import java.util.Arrays;
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
复杂度分析
- 时间复杂度:
O(n * sqrt(n)) - 空间复杂度:
O(n)
解法二:BFS(把问题看成最短路径)
原理
把每个数字看作图中的一个节点。
从 n 出发,每次可以减去一个平方数 s(s <= 当前值),到达新节点 cur - s。
每减一次平方数,相当于走一条边。问题就变成:
从
n到0的最短路径长度是多少?
BFS 天然求无权图最短路,层数即答案。
BFS 时序示意
Java 代码
import java.util.*;
class Solution {
public int numSquares(int n) {
Queue<Integer> queue = new LinkedList<>();
boolean[] visited = new boolean[n + 1];
queue.offer(n);
visited[n] = true;
int step = 0;
while (!queue.isEmpty()) {
int size = queue.size();
step++;
for (int k = 0; k < size; k++) {
int cur = queue.poll();
for (int j = 1; j * j <= cur; j++) {
int next = cur - j * j;
if (next == 0) return step;
if (!visited[next]) {
visited[next] = true;
queue.offer(next);
}
}
}
}
return step;
}
}
复杂度分析
- 时间复杂度:最坏
O(n * sqrt(n)) - 空间复杂度:
O(n)
解法三:数学定理(性能很强)
原理
利用两个经典定理:
-
拉格朗日四平方定理:任意正整数都可表示为最多 4 个平方数之和
→ 答案只可能是1,2,3,4 -
勒让德三平方定理推论:若
n = 4^a(8b+7),则n不能表示为 3 个平方数之和
→ 这种情况答案一定是4
判定流程:
- 若
n本身是完全平方数,答案1 - 把
n中因子4全部除掉(while n % 4 == 0) - 若
n % 8 == 7,答案4 - 枚举
a^2,判断n-a^2是否平方数,若是则答案2 - 否则答案
3
判定流程图
Java 代码
class Solution {
public int numSquares(int n) {
if (isSquare(n)) return 1;
// 去掉 4 的因子
while (n % 4 == 0) {
n /= 4;
}
// n = 8b + 7 的形式 => 4
if (n % 8 == 7) return 4;
// 检查是否可由两个平方数组成
for (int a = 1; a * a <= n; a++) {
int b = n - a * a;
if (isSquare(b)) return 2;
}
// 不是1、2、4,则是3
return 3;
}
private boolean isSquare(int x) {
int r = (int) Math.sqrt(x);
return r * r == x;
}
}
复杂度分析
- 时间复杂度:
O(sqrt(n)) - 空间复杂度:
O(1)
不同解法的实现复杂度与性能对比
| 解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 实现复杂度 | 性能表现 | 适用场景 |
|---|---|---|---|---|---|---|
| 动态规划 | dp[i] 最优子结构 | O(n*sqrt(n)) | O(n) | 中等 | 稳定、通用 | 面试高频,易讲清楚 |
| BFS | 无权图最短路(层数) | 最坏 O(n*sqrt(n)) | O(n) | 中等偏上 | 可能较快,依赖剪枝与数据分布 | 想用图论视角时 |
| 数学定理 | 四平方/三平方定理判定 | O(sqrt(n)) | O(1) | 较高(需要定理) | 通常最快 | 追求极致性能、理解数学结论后使用 |
总结
如果你想要最稳妥的解法:用 动态规划。
如果你想要最优复杂度:用 数学定理法。
如果你想训练图论建模能力:可以写 BFS。
在 LeetCode 279 中,三种方法都很经典,建议至少掌握 DP + 数学法。
816

被折叠的 条评论
为什么被折叠?



