📌 题目链接:279. 完全平方数 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:动态规划、数学、完全平方数、四平方和定理
⏱️ 目标时间复杂度:O(n√n)(DP)或 O(√n)(数学法)
💾 空间复杂度:O(n)(DP)或 O(1)(数学法)
在 LeetCode 的经典题目 279. 完全平方数 中,我们被要求找出“和为 n 的完全平方数的最少数量”。这道题看似简单,却蕴含了两种截然不同的解题哲学:动态规划的通用性 与 数学定理的优雅性。无论你是准备面试还是夯实算法基础,这道题都值得你深入掌握。
🔗 题目链接
🔍 题目分析
给定一个正整数 n,我们需要将其表示为若干个完全平方数(如 1, 4, 9, 16...)之和,并使得所用的完全平方数数量最少。
例如:
n = 12→4 + 4 + 4→ 最少 3 个n = 13→4 + 9→ 最少 2 个
关键点:
- 不要求平方数互不相同;
- 要求最少数量,而非所有组合;
n的范围是1 ≤ n ≤ 10⁴,适合使用 DP 或数学优化。
⚙️ 核心算法及代码讲解
本题有两种主流解法:
✅ 方法一:动态规划(通用、易理解、面试高频)
思路核心
定义 f[i] 表示组成数字 i 所需的最少完全平方数个数。
状态转移方程:
f[i] = min{ f[i - j²] } + 1 ,其中 j² ≤ i
边界条件:f[0] = 0(虽然 0 不能由正平方数组成,但作为递推起点必须设为 0)
代码详解(C++)
vector<int> f(n + 1); // f[i] 表示和为 i 的最少平方数个数
for (int i = 1; i <= n; i++) {
int minn = INT_MAX; // 初始化当前最小值为无穷大
for (int j = 1; j * j <= i; j++) { // 枚举所有可能的平方数 j²
minn = min(minn, f[i - j * j]); // 从子问题 f[i - j²] 转移
}
f[i] = minn + 1; // 加上当前使用的 j²,总数 +1
}
return f[n];
✅ 优点:逻辑清晰,适用于任意“硬币找零”类问题变种。
⚠️ 缺点:时间复杂度较高(O(n√n)),但在 n ≤ 10⁴ 下完全可接受。
✅ 方法二:数学法(基于四平方和定理,极致优化)
📜 四平方和定理(Lagrange's Four-Square Theorem)
任意正整数都可以表示为至多 4 个完全平方数之和。
更进一步,Legendre 补充了判定条件:
当且仅当
n = 4ᵏ × (8m + 7)时,n 必须用 4 个平方数表示;否则最多用 3 个。
因此,答案只能是 1、2、3 或 4。
判定流程:
- 是否为完全平方数? → 是则返回 1。
- 是否满足
n = 4ᵏ(8m+7)? → 是则返回 4。 - 能否拆成两个平方数之和? → 枚举
a,检查n - a²是否为平方数 → 是则返回 2。 - 否则 → 必为 3。
代码详解(C++)
// 判断 x 是否为完全平方数
bool isPerfectSquare(int x) {
int y = sqrt(x); // 取整数平方根
return y * y == x; // 验证平方是否等于原数
}
// 判断是否满足 n = 4^k * (8m + 7)
bool checkAnswer4(int x) {
while (x % 4 == 0) { // 不断除以 4,直到无法整除
x /= 4;
}
return x % 8 == 7; // 检查剩余部分是否 ≡7 (mod 8)
}
int numSquares(int n) {
if (isPerfectSquare(n)) return 1; // 情况1:本身就是平方数
if (checkAnswer4(n)) return 4; // 情况2:必须用4个
for (int i = 1; i * i <= n; i++) { // 情况3:尝试拆成两个
int j = n - i * i;
if (isPerfectSquare(j)) return 2;
}
return 3; // 情况4:只能是3
}
✅ 优点:时间复杂度 O(√n),空间 O(1),极其高效。
💡 面试加分项:能说出四平方和定理,展现数学素养!
🧩 解题思路(分步拆解)
动态规划法步骤:
- 初始化:创建长度为
n+1的数组f,f[0] = 0。 - 外层循环:遍历
i从 1 到n,计算每个f[i]。 - 内层循环:枚举所有
j满足j² ≤ i。 - 状态转移:
f[i] = min(f[i - j²]) + 1。 - 返回结果:
f[n]即为答案。
数学法步骤:
- 检查 1:
n是否为完全平方数? - 检查 4:不断除以 4,看是否最终模 8 余 7。
- 检查 2:枚举
a ∈ [1, √n],看n - a²是否为平方数。 - 默认 3:若以上都不满足,则答案为 3。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 动态规划 | O(n√n) | O(n) | 通用解法,适合变形题(如限制平方数种类) |
| 数学法 | O(√n) | O(1) | 本题最优解,体现数学洞察力 |
💡 面试建议:先写 DP 解法(稳妥),再提数学优化(惊艳)!
💻 代码
C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 方法一:动态规划
class SolutionDP {
public:
int numSquares(int n) {
vector<int> f(n + 1);
for (int i = 1; i <= n; i++) {
int minn = INT_MAX;
for (int j = 1; j * j <= i; j++) {
minn = min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
}
};
// 方法二:数学法
class SolutionMath {
public:
bool isPerfectSquare(int x) {
int y = sqrt(x);
return y * y == x;
}
bool checkAnswer4(int x) {
while (x % 4 == 0) {
x /= 4;
}
return x % 8 == 7;
}
int numSquares(int n) {
if (isPerfectSquare(n)) {
return 1;
}
if (checkAnswer4(n)) {
return 4;
}
for (int i = 1; i * i <= n; i++) {
int j = n - i * i;
if (isPerfectSquare(j)) {
return 2;
}
}
return 3;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
SolutionMath sol;
// 测试用例
cout << sol.numSquares(12) << "\n"; // 输出: 3
cout << sol.numSquares(13) << "\n"; // 输出: 2
cout << sol.numSquares(1) << "\n"; // 输出: 1
cout << sol.numSquares(7) << "\n"; // 输出: 4 (7 = 4^0 * (8*0+7))
cout << sol.numSquares(6) << "\n"; // 输出: 3 (4+1+1)
return 0;
}
JavaScript
// 方法一:动态规划
var numSquares = function(n) {
const f = new Array(n + 1).fill(0);
for (let i = 1; i <= n; i++) {
let minn = Number.MAX_VALUE;
for (let j = 1; j * j <= i; j++) {
minn = Math.min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
};
// 方法二:数学法
const isPerfectSquare = (x) => {
const y = Math.floor(Math.sqrt(x));
return y * y === x;
};
const checkAnswer4 = (x) => {
while (x % 4 === 0) {
x /= 4;
}
return x % 8 === 7;
};
var numSquares = function(n) {
if (isPerfectSquare(n)) {
return 1;
}
if (checkAnswer4(n)) {
return 4;
}
for (let i = 1; i * i <= n; i++) {
let j = n - i * i;
if (isPerfectSquare(j)) {
return 2;
}
}
return 3;
};
🎯 面试高频考点总结
- 动态规划建模能力:能否将“最少数量”转化为状态转移?
- 数学定理应用:是否知道四平方和定理?能否快速判断 4 的情况?
- 边界处理:
f[0] = 0的合理性?sqrt取整误差如何避免? - 优化意识:在 DP 基础上,能否想到数学剪枝?
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!