题目描述:
给你一个整数
n,返回 和为n的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1、4、9和16都是完全平方数,而3和11不是。示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9
解题思路:
方法一:动态规划(完全背包)
核心思路:
状态定义:
dp[i] = 凑出 i 所需的最少完全平方数数量。
状态转移:
对于每个 i,枚举所有小于等于 i 的完全平方数 j²:
dp[i] = min(dp[i], dp[i - j²] + 1)
初始化:
-
dp[0] = 0(凑出 0 需要 0 个) -
其他初始化为
INT_MAX或i(最坏情况全用 1)
具体过程示例:
n = 12
dp[0] = 0
dp[1] = dp[0] + 1 = 1(1 = 1)
dp[2] = dp[1] + 1 = 2(2 = 1+1)
dp[3] = dp[2] + 1 = 3(3 = 1+1+1)
dp[4] = min(dp[3]+1, dp[0]+1) = 1(4 = 4)
dp[5] = min(dp[4]+1, dp[1]+1) = 2(5 = 4+1)
...
dp[12] = min(dp[11]+1, dp[8]+1, dp[3]+1)
= min(?, ?, 3+1) = 3(12 = 4+4+4)✅
代码实现:
写法1:动态规划(标准)
cpp
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
写法2:先遍历平方数(完全背包思路)
cpp
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int j = 1; j * j <= n; j++) {
int square = j * j;
for (int i = square; i <= n; i++) {
dp[i] = min(dp[i], dp[i - square] + 1);
}
}
return dp[n];
}
};
两种写法的区别:
-
写法1:外层遍历
i,内层遍历平方数 -
写法2:外层遍历平方数,内层遍历
i(标准完全背包)
本题两种都可以 ,因为求的是组合数(顺序不重要)。
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n × √n) | 外层 n,内层最多 √n 个平方数 |
| 空间复杂度 | O(n) | dp 数组 |
关键细节:
1. 为什么 dp[0] = 0?
凑出 0 不需要任何平方数。
2. 为什么初始化为 INT_MAX?
因为要求最少 数量,初始化为最大值方便取 min。
3. 为什么内层循环是 j * j <= i?
因为只需要考虑小于等于 i 的平方数。
4. 和「零钱兑换」的区别
| 题目 | 区别 |
|---|---|
| 322. 零钱兑换 | 硬币面额任意,求最少硬币数 |
| 279. 完全平方数 | 面额是平方数,求最少数量 |
本质完全一样,都是完全背包求最少数量。
方法二:BFS(层序遍历)
思路:
把 n 看作起点,每次减去一个平方数,看最少几步到达 0。
代码实现:
cpp
class Solution {
public:
int numSquares(int n) {
queue<int> q;
q.push(n);
vector<bool> visited(n + 1, false);
visited[n] = true;
int level = 0;
while (!q.empty()) {
int size = q.size();
level++;
for (int i = 0; i < size; i++) {
int curr = q.front();
q.pop();
for (int j = 1; j * j <= curr; j++) {
int next = curr - j * j;
if (next == 0) return level;
if (!visited[next]) {
visited[next] = true;
q.push(next);
}
}
}
}
return level;
}
};
复杂度:时间 O(n × √n),空间 O(n)
方法三:数学(拉格朗日四平方定理)
定理 :每个正整数都可以表示为至多四个平方数之和。
所以答案只可能是 1、2、3、4。
-
答案是 1:
n本身是完全平方数 -
答案是 2:
n = a² + b² -
答案是 4:
n = 4^k × (8m + 7) -
否则答案是 3
代码实现:
cpp
class Solution {
public:
int numSquares(int n) {
// 答案是 1
int sqrtN = sqrt(n);
if (sqrtN * sqrtN == n) return 1;
// 答案是 4
while (n % 4 == 0) n /= 4;
if (n % 8 == 7) return 4;
// 答案是 2
for (int i = 1; i * i <= n; i++) {
int j = sqrt(n - i * i);
if (i * i + j * j == n) return 2;
}
// 答案是 3
return 3;
}
};
复杂度:时间 O(√n),空间 O(1)
三种方法对比:
| 方法 | 时间复杂度 | 空间复杂度 | 推荐度 |
|---|---|---|---|
| 动态规划 | O(n × √n) | O(n) | ⭐⭐⭐⭐⭐ |
| BFS | O(n × √n) | O(n) | ⭐⭐⭐⭐ |
| 数学定理 | O(√n) | O(1) | ⭐⭐⭐⭐ |
总结:
| 要点 | 说明 |
|---|---|
| 核心思想 | 完全背包,dp[i] = min(dp[i], dp[i-j²]+1) |
| 初始化 | dp[0] = 0,其余 INT_MAX |
| 时间复杂度 | O(n × √n) |
| 空间复杂度 | O(n) |