Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ...) which sum to n.
For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
这一题目测方法很多,我的第一感觉就是使用动态规划来做。
方法一:
传统动态规划。
首先我们需要一个辅助数组,dp[n+1],存储针对当前数字,最小需要几个完全平方数来拼凑。
动态规划的 方程是:
dp[k]=min(dp[k-i]+dp[i],current_max_value)
针对上面的方程的解释是:对于当前数字k,给定变量i,从1遍历到k-1,获取dp[k-i]+dp[i]的最小值,作为dp[k]的值。依次类推,对于k从2遍历到n,最后返回dp[n]即可。
值得注意的几点:
- 辅助数组初始化的时候需要判断当前的数本身是不是完全平方数,如果是的话,dp[k]置为1。至于如何判断是否为完全平方数,遍历1-n并不是一个明智的选择,因为大部分的数组都不是完全平方数。因此,需要一个while循环,给定变量k,从1开始,dp[k]=1,k++,直到k*k>n,结束就好。
- 在计算dp[k-i]+dp[i]的时候,实际上如果有一个等于0,说明不可能凑成,dp[k]应该是等于0的。
- 在有额外存储空间的情况下,不要企图利用递归直接输出dp[n],因为递归深度过深,会造成栈溢出,后面会有一个典型的错误案例。
public class Solution {
int[] nDp;
public int numSquares(int n) {
nDp = new int[n + 1];
// init nDP, if it is perfect square, set it to 1
int nIndex = 1;
while (true) {
int nCur = nIndex * nIndex;
if (nCur <= n) {
nDp[nCur] = 1;
} else {
break;
}
nIndex++;
}
for (int i = 1; i < nDp.length; i++) {
dp(i);
}
return nDp[n];
}
private int dp(int n) {
if (nDp[n] > 0) {
return nDp[n];
} else {
int nMax = Integer.MAX_VALUE;
for (int i = 1; i <= n / 2; i++) {
int nDpx = nDp[i];
int nDpy = nDp[n - i];
if (nDpx == 0 || nDpy == 0) {
nMax = Math.max(0, nMax);
} else {
nMax = Math.min(nDpx + nDpy, nMax);
}
}
nDp[n] = nMax;
return nMax;
}
}
}
另外一点很重要的,在上面的29行的for循环中,有一个很重要的优化,把i换成了i*i(原因和上面的第一点一样),因为大部分都不是完全平方数,这样可以避免重复计算
for (int i = 1; i*i <= n / 2; i++) {
int nDpx = nDp[i*i];
int nDpy = nDp[n - i*i];
if (nDpx == 0 || nDpy == 0) {
nMax = Math.max(0, nMax);
} else {
nMax = Math.min(nDpx + nDpy, nMax);
}
}
栈溢出错误例子(StackOverflowError):
public class Solution {
public class Solution {
int[] nDp;
public int numSquares(int n) {
nDp = new int[n + 1];
for (int i = 0; i < nDp.length; i++) {
nDp[i] = -1;
}
// init nDP, if it is perfect square, set it to 1
int nIndex = 1;
while (true) {
int nCur = nIndex * nIndex;
if (nCur <= n) {
nDp[nCur] = 1;
} else {
break;
}
nIndex++;
}
return dp(n);
}
private int dp(int n) {
if (nDp[n] != -1) {
return nDp[n];
} else {
int nMax = Integer.MAX_VALUE;
for (int i = 1; i <= n / 2; i++) {
int nDpx = dp(i);
int nDpy = dp(n - i);
if (nDpx == 0 || nDpy == 0) {
nMax = Math.max(0, nMax);
} else {
nMax = Math.min(nDpx + nDpy, nMax);
}
}
nDp[n] = nMax;
return nMax;
}
}
}
可以看到上面代码的21行,直接调用的dp(n),也就是说n前面的大部分数都没有算(大部分都是非完全平方数),遇到了会继续递归,会造成递归层数过多,函数递归是保存在栈中的,栈溢出。这样做的话,也没有充分的使用辅助数组dp[]。
方法二:
定理法:请参考,四平方和定理 (英语:Lagrange's four-square theorem) 说明每个正整数均可表示为4个整数的平方和。它是费马多边形数定理和华林问题的特例,和拉格朗日三平方和定理 Legendre's three-square theorem。