题目
题解
方法一:动态规划
思路
1,对于正整数N, 所有的解都是 N = 一个整数的平方 + 另一个整数; 直白点, N = AxA + B
2, 而B又是由 “一个整数的平方 + 另一个整数” 组成的; 那么, B = CxC + D
3,总结下就是:N = IxI + N’ 而 N’ = IxI + N’’
4, 本题要解的问题:正整数N最少由多个平方数相加;
5, 那么,N的最优解 = 1 + (N’的最优解)。而N’肯定小于N。
6, 所以本题的思路就是,对每一个N,观察1到N-1中,谁的解最小,那么N的解就是它+1.
7, 但是我们没必要1到N-1中的每一个数都去观察,因为有些组合不满足N = IxI + N’,譬如12 = 2+N’是不需要的,因为2不是某个数的平方。所以我们观察的范围要大大减小。
拿12举例,我们只能观察:
12 = 1 + 11
12 = 4 + 8
12 = 9 + 3
我们要得出3,8,11中谁的解最优,那么12的解就是它+1。
8, 我们从1到N计算, 2的解从1里找,3的解从[2,1]里找,4的解从[3,2,1]里找,依次类推,最后算到N的解即可。
数学理解
//假设最小公式值m = ƒ(n)
//那么n的值满足下列公式 ∑(A[i] * A[i]) = n
//令 k 为满足最小值 m 的时候,最大的平方数 。 令 d + k * k = n ; d >= 0;
// 注意:一定要是满足m最小的时候的k值,一味的取最大平方数,就是贪心算法了
//得出 f(d) + f(k*k) = f(n);
//显然 f(k*k) = 1; 则 f(d) + 1 = f(n); 因为 d = n - k*k;
//则可以推出ƒ(n - k * k) + 1 = ƒ(n) ; 且 k * k <= n;
我们来理解一下动态规划方程dp[i] = Math.min(dp[i], dp[i - j * j] + 1)
#动态方程的全写版本应该是:
for(int j = 1;j*j<=i;j++){
dp[i] = Math.main(dp[i],dp[i-j*j]+dp[j*j];
}
# dp[i]:表示完全平方数和为i的 最小个数
# 初始状态dp[i]均取最大值i,即 1+1+...+1,i个1; dp[0] = 0
# dp[i] = min(dp[i], dp[i-j*j]+1),其中, j是平方数, j=1~k,其中k*k要保证 <= i
# 意思就是:(完全平方数和为i的最小个数) 等于 (当前完全平方数和为i的最大个数)dp[i] 与 (完全平方数和为 i - j * j 的最小个数 + 完全平方数和为 j * j的 最小个数)的最小个数
# 可以看到 dp[j*j] 是等于1的
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1]; // 默认初始化值都为0
for (int i = 1; i <= n; i++) {
dp[i] = i; // 最坏的情况就是每次+1
for (int j = 1; i - j * j >= 0; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 动态转移方程
}
}
return dp[n];
}
}
方法二:贪心枚举
递归解决方法为我们理解问题提供了简洁直观的方法。我们仍然可以用递归解决这个问题。为了改进上述暴力枚举解决方案,我们可以在递归中加入贪心。我们可以将枚举重新格式化如下:
从一个数字到多个数字的组合开始,一旦我们找到一个可以组合成给定数字 n
的组合,那么我们可以说我们找到了最小的组合,因为我们贪心的从小到大的枚举组合。
为了更好的解释,我们首先定义一个名为 is_divided_by(n, count)
的函数,该函数返回一个布尔值,表示数字 n
是否可以被一个数字 count
组合,而不是像前面函数 numSquares(n)
返回组合的确切大小。
与递归函数 numSquare(n)
不同,is_divided_by(n, count)
的递归过程可以归结为底部情况(即 count==1
)更快。
下面是一个关于函数 is_divided_by(n, count)
的例子,它对 输入 n=5
和 count=2
进行了分解。
通过这种重新构造的技巧,我们可以显著降低堆栈溢出的风险。
算法:
- 首先,我们准备一个小于给定数字
n
的完全平方数列表(称为square_nums
)。 - 在主循环中,将组合的大小(称为
count
)从1
迭代到n
,我们检查数字n
是否可以除以组合的和,即is_divided_by(n, count)
。 - 函数
is_divided_by(n, count)
可以用递归的形式实现,汝上面所说。 - 在最下面的例子中,我们有
count==1
,我们只需检查数字n
是否本身是一个完全平方数。可以在square_nums
中检查,即 n \in \text{square_nums}n∈square_nums。如果square_nums
使用的是集合数据结构,我们可以获得比n == int(sqrt(n)) ^ 2
更快的运行时间。
关于算法的正确性,通常情况下,我们可以用反证法来证明贪心算法。这也不例外。假设我们发现 count=m
可以除以 n
,并且假设在以后的迭代中存在另一个 count=p
也可以除以 n
,并且这个数的组合小于找到的数,即 p<m
。如果给定迭代的顺序,count = p
会在 count=m
之前被发现,因此,该算法总是能够找到组合的最小大小。
下面是一些示例实现。Python 解决方案需要大约 70ms,这比当时大约 90% 的提交要快。
class Solution {
Set<Integer> square_nums = new HashSet<Integer>();
protected boolean is_divided_by(int n, int count) {
if (count == 1) {
return square_nums.contains(n);
}
for (Integer square : square_nums) {
if (is_divided_by(n - square, count - 1)) {
return true;
}
}
return false;
}
public int numSquares(int n) {
this.square_nums.clear();
for (int i = 1; i * i <= n; ++i) {