问题分析
题目是 完全平方数(Perfect Squares):
给定一个正整数 n,找到最少的完全平方数的个数,使它们的和等于 n。
例如:
• n = 12 → 12 = 4 + 4 + 4 → 最少 3 个完全平方数(3^2 不行,因为 9+3 不是完全平方数)。
• n = 13 → 13 = 4 + 9 → 最少 2 个完全平方数。
解题思路
1. 动态规划 (Dynamic Programming)
• 定义状态:
• dp[i] 表示最少使用多少个完全平方数来表示 i。
• 状态转移方程:
• 对于任意 i,可以由一个更小的数 i - j^2 加上 j^2 来得到:
• 其中 j^2 是小于等于 i 的完全平方数。
• 初始化:
• dp[0] = 0,因为 0 需要 0 个完全平方数。
• dp[i] 默认初始化为 i,因为 i 最坏情况是 1 + 1 + ... + 1(全是 1 的平方)。
• 最终答案:
• dp[n] 即为 n 所需的最少完全平方数个数。
代码解析
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1);
for(int i = 1; i <= n; i++) {
dp[i] = i; // 最坏情况:全是 1
for(int j = 1; i - j * j >= 0; j++) {
dp[i] = min(dp[i], dp[i - j * j] + 1); // 核心状态转移方程
}
}
return dp[n];
}
};
运行步骤
假设输入 n = 12,动态规划表 dp 变化如下:
i | dp[i] 计算方式 | dp[i] 结果 |
---|---|---|
1 | dp[1] = min(1, dp[0] + 1) = 1 | 1 |
2 | dp[2] = min(2, dp[1] + 1) = 2 | 2 |
3 | dp[3] = min(3, dp[2] + 1) = 3 | 3 |
4 | dp[4] = min(4, dp[0] + 1) = 1 | 1 |
5 | dp[5] = min(5, dp[1] + 1) = 2 | 2 |
6 | dp[6] = min(6, dp[2] + 1) = 3 | 3 |
7 | dp[7] = min(7, dp[3] + 1) = 4 | 4 |
8 | dp[8] = min(8, dp[4] + 1) = 2 | 2 |
9 | dp[9] = min(9, dp[0] + 1) = 1 | 1 |
10 | dp[10] = min(10, dp[6] + 1) = 2 | 2 |
11 | dp[11] = min(11, dp[7] + 1) = 3 | 3 |
12 | dp[12] = min(12, dp[8] + 1) = 3 | 3 |
最终 dp[12] = 3,即 12 = 4 + 4 + 4,需要 3 个完全平方数。
时间复杂度
• 外层循环:i 从 1 到 n,O(n)。
• 内层循环:j^2 ≤ i,最多 O(√n)。
总时间复杂度:
空间复杂度:O(n)(dp 数组占用的空间)。
优化方案
可以用 四平方定理 + 贪心 + BFS 进一步优化:
1. 四平方和定理(拉格朗日定理):任何正整数 n 至多由 4 个完全平方数组成。
• 如果 n 是完全平方数,直接返回 1。
• 如果 n 可以拆成两个完全平方数,返回 2。
• 如果 n 满足 n = 4^a × (8b + 7),返回 4。
• 否则返回 3。
class Solution {
public:
bool isPerfectSquare(int n) {
int sq = sqrt(n);
return sq * sq == n;
}
int numSquares(int n) {
// 1. 如果是完全平方数,直接返回 1
if (isPerfectSquare(n)) return 1;
// 2. 检查是否能由两个完全平方数组成
for (int i = 1; i * i <= n; i++) {
if (isPerfectSquare(n - i * i)) return 2;
}
// 3. 如果 n 满足 4^a * (8b + 7),则返回 4
while (n % 4 == 0) n /= 4; // 去掉 4^a
if (n % 8 == 7) return 4;
// 4. 否则返回 3
return 3;
}
};
优化后的时间复杂度
• 检查是否是完全平方数: O(1)
• 检查是否由两个平方数组成: O(√n)
• 四平方定理判定: O(log n)
最终优化后的时间复杂度:O(√n),比 O(n√n) 提高了很多。
总结
1. 动态规划解法(O(n√n))适用于一般情况:
• dp[i] = min(dp[i], dp[i - j^2] + 1)
• 通过遍历 1, 4, 9, ... 更新 dp。
2. 数学优化(O(√n))基于 四平方定理:
• 直接判断 n 是否满足特殊情况,避免 DP 计算。
在实际应用中:
• n 比较小(如 ≤ 10^4)时,DP 适用。
• n 很大(如 10^9),用数学方法更快。