Medium | LeetCode 279. 完全平方数 | 动态规划 | BFS
279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n
,返回和为 n
的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
解题思路
方法一:暴力枚举法
先转化问题:给定一个完全平方数列表和正整数N, 求出完全平方数组合成N的组合, 要求组合中的解拥有完全平方数的最小个数。
采用动态规划的方法求解。这种方法本质上是暴力枚举所有的组合。
状态转移方程如下:
这里采用递归的方式实现动态规划。
class Solution1 {
private int[] squareList;
public int numSquares(int n) {
double sqrt = Math.sqrt(n);
// 先生成平方数列表 最大的完全平方数刚好不大于N
squareList = new int[Double.valueOf(sqrt).intValue()];
for (int i = 1; i <= squareList.length; i++) {
squareList[i-1] = i * i;
}
return findSquare(n);
}
public int findSquare(int n) {
if (isSquare(n)) {
// 递归出口, N是一个包含在完全平方数列表中的数字
return 1;
}
int minNumber = Integer.MAX_VALUE;
// 遍历完全平方数列表的所有数
for (int square : squareList) {
if (square < n) {
// 计算 n - square 需由多少个完全平方数组成
minNumber = Math.min(findSquare(n - square) + 1, minNumber);
}
}
return minNumber;
}
public boolean isSquare(int n) {
int left = 0, right = squareList.length - 1;
while (left <= right) {
int mid = (left + right) >> 1;
if (n < squareList[mid]) {
left = mid + 1;
} else if (n > squareList[mid]) {
right = mid - 1;
} else {
return true;
}
}
return false;
}
}
方法二:动态规划
方法一中实际上有很多的结果重复计算。一种解决的办法是使用int[] dp = new int[n+1]
的数组来保留中间的计算结果。下面的代码的思想基本和第一种办法相同。一个不同点时, 这里采用自底向上的迭代的方法实现。
public int numSquares(int n) {
// 动态规划数组, dp[i]表示组成数字i的完全平方数的最小的个数
int dp[] = new int[n + 1];
// 初始化为无穷大数组
Arrays.fill(dp, Integer.MAX_VALUE);
// bottom case
dp[0] = 0;
// 预先建立平方数数组
int max_square_index = (int) Math.sqrt(n) + 1;
int square_nums[] = new int[max_square_index];
for (int i = 1; i < max_square_index; ++i) {
square_nums[i] = i * i;
}
for (int i = 1; i <= n; ++i) {
// 遍历完全平方数列表, 尝试使用每一个不大于 i 的完全平方数
for (int s = 1; s < max_square_index; ++s) {
if (i < square_nums[s])
// 剪枝, 当完全平方数列表的某个数大于数字i时,后面的数字也就不用计算了。
break;
dp[i] = Math.min(dp[i], dp[i - square_nums[s]] + 1);
}
}
return dp[n];
}
方法三: 贪心枚举
从一个数字到多个数字的组合开始,一旦我们找到一个可以组合成给定数字 n 的组合,那么我们可以说我们找到了最小的组合,因为我们贪心的从小到大的枚举组合。
class Solution3 {
private Set<Integer> square_nums = new HashSet<Integer>();
public int numSquares(int n) {
this.square_nums.clear();
for (int i = 1; i * i <= n; ++i) {
this.square_nums.add(i * i);
}
int count = 1;
for (; count <= n; ++count) {
// 依次提高完全平方数的个数, 先看这个数能不能被1个完全数组合, 再看2个, 3个
if (is_divided_by(n, count))
return count;
}
return count;
}
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;
}
}
方法四:贪心 + BFS
把方法三的思路, 描绘成 一棵树, 然后通过遍历的方式, 找到叶节点是完全平方数的最小深度。
- 首先,我们准备小于给定数字 n 的完全平方数列表(即 square_nums)。
- 然后创建 queue 遍历,该变量将保存所有剩余项在每个级别的枚举。
- 在主循环中,我们迭代 queue 变量。在每次迭代中,我们检查余数是否是一个完全平方数。
3.1 如果余数不是一个完全平方数,就用其中一个完全平方数减去它,得到一个新余数,
3.2 然后将新余数添加到 next_queue 中,以进行下一级的迭代。
一旦遇到一个完全平方数的余数,我们就会跳出循环,这也意味着我们找到了解。
时间复杂度和空间复杂度和方法三相当
public int numSquares(int n) {
ArrayList<Integer> square_nums = new ArrayList<Integer>();
for (int i = 1; i * i <= n; ++i) {
square_nums.add(i * i);
}
Set<Integer> queue = new HashSet<Integer>();
// 初始时, queue里的数字,
queue.add(n);
int level = 0;
while (queue.size() > 0) {
// 记录当前遍历的层次
level += 1;
Set<Integer> next_queue = new HashSet<Integer>();
for (Integer remainder : queue) {
// 对于队列中的每个数
for (Integer square : square_nums) {
// 用完全平方数列表的每个数字
if (remainder.equals(square)) {
// 如果发现了当前层次的数字是一个完全平方数, 则返回当前的层次
return level;
} else if (remainder < square) {
break;
} else {
// 如果当前数字不是完全平方数, 将剩余项添加到下一层的队列
next_queue.add(remainder - square);
}
}
}
queue = next_queue;
}
return level;
}
方法五:数学运算
1770 年,Joseph Louis Lagrange证明了一个定理,称为四平方和定理,也称为 Bachet 猜想,它指出每个自然数都可以表示为四个整数平方和:
在 1797 年,Adrien Marie Legendre用他的三平方定理完成了四平方定理,证明了正整数可以表示为三个平方和的一个特殊条件:
n != 4^k * (8*m + 7) 等价于 n 为三数平方和
至今没有任何定理能解决一个数是否是两个数的平方和的问题, 只能使用枚举的办法
所以算法思路为: 首先检查 数字 n 的形式是否为 n = 4^k * (8m+7) 如果是, 则直接返回4
进一步检查这个数本身是否是一个完全平方数,或者这个数是否可以分解为两个完全平方数和。
在底部的情况下,这个数可以分解为 3 个平方和。
时间复杂度: O(N ^ 1/2)
空间复杂度: O(1)
protected boolean isSquare(int n) {
int sq = (int) Math.sqrt(n);
return n == sq * sq;
}
public int numSquares(int n) {
// four-square and three-square theorems.
while (n % 4 == 0)
n /= 4;
if (n % 8 == 7)
return 4;
if (this.isSquare(n))
return 1;
// enumeration to check if the number can be decomposed into sum of two squares.
for (int i = 1; i * i <= n; ++i) {
if (this.isSquare(n - i * i))
return 2;
}
// bottom case of three-square theorem.
return 3;
}