题目描述(中等难度)

279. Perfect Squares - 图1

把一个数分解成若干个平方数的和,可能有多种方案,找到所需平方数的最少的方案,将最少个数返回。

解法一 回溯法

相当于一种暴力的方法,去考虑所有的分解方案,找出最小的解,举个例子。

  1. n = 12
  2. 先把 n 减去一个平方数,然后求剩下的数分解成平方数和所需的最小个数
  3. n 减去 1, 然后求出 11 分解成平方数和所需的最小个数,记做 n1
  4. 那么当前方案总共需要 n1 + 1 个平方数
  5. n 减去 4, 然后求出 8 分解成平方数和所需的最小个数,记做 n2
  6. 那么当前方案总共需要 n2 + 1 个平方数
  7. n 减去 9, 然后求出 3 分解成平方数和所需的最小个数,记做 n3
  8. 那么当前方案总共需要 n3 + 1 个平方数
  9. 下一个平方数是 16, 大于 12, 不能再分了。
  10. 接下来我们只需要从 (n1 + 1), (n2 + 1), (n3 + 1) 三种方案中选择最小的个数,
  11. 此时就是 12 分解成平方数和所需的最小个数了
  12. 至于求 1183 分解成最小平方数和所需的最小个数继续用上边的方法去求
  13. 直到如果求 0 分解成最小平方数的和的个数, 返回 0 即可

代码的话,就是回溯的写法,或者说是 DFS

  1. public int numSquares(int n) {
  2. return numSquaresHelper(n);
  3. }
  4. private int numSquaresHelper(int n) {
  5. if (n == 0) {
  6. return 0;
  7. }
  8. int count = Integer.MAX_VALUE;
  9. //依次减去一个平方数
  10. for (int i = 1; i * i <= n; i++) {
  11. //选最小的
  12. count = Math.min(count, numSquaresHelper(n - i * i) + 1);
  13. }
  14. return count;
  15. }

当然上边的会造成超时,很多解会重复的计算,之前也遇到很多这种情况了。我们需要 memoization 技术,也就是把过程中的解利用 HashMap 全部保存起来即可。

  1. public int numSquares(int n) {
  2. return numSquaresHelper(n, new HashMap<Integer, Integer>());
  3. }
  4. private int numSquaresHelper(int n, HashMap<Integer, Integer> map) {
  5. if (map.containsKey(n)) {
  6. return map.get(n);
  7. }
  8. if (n == 0) {
  9. return 0;
  10. }
  11. int count = Integer.MAX_VALUE;
  12. for (int i = 1; i * i <= n; i++) {
  13. count = Math.min(count, numSquaresHelper(n - i * i, map) + 1);
  14. }
  15. map.put(n, count);
  16. return count;
  17. }

解法二 动态规划

理解了解法一的话,很容易改写成动态规划。递归相当于先压栈压栈然后出栈出栈,动态规划可以省去压栈的过程。

动态规划的转移方程就对应递归的过程,动态规划的初始条件就对应递归的出口。

  1. public int numSquares(int n) {
  2. int dp[] = new int[n + 1];
  3. Arrays.fill(dp, Integer.MAX_VALUE);
  4. dp[0] = 0;
  5. //依次求出 1, 2... 直到 n 的解
  6. for (int i = 1; i <= n; i++) {
  7. //依次减去一个平方数
  8. for (int j = 1; j * j <= i; j++) {
  9. dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
  10. }
  11. }
  12. return dp[n];
  13. }

这里还提出来一种 Static Dynamic Programming,主要考虑到测试数据有多组,看一下 leetcode 全部代码的逻辑。

点击下图箭头的位置。

279. Perfect Squares - 图2

然后会看到下边的代码。

  1. class Solution {
  2. public int numSquares(int n) {
  3. int dp[] = new int[n + 1];
  4. Arrays.fill(dp,Integer.MAX_VALUE);
  5. dp[0] = 0;
  6. for (int i = 1; i <= n; i++) {
  7. for (int j = 1; j * j <= i; j++) {
  8. dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
  9. }
  10. }
  11. return dp[n];
  12. }
  13. }
  14. public class MainClass {
  15. public static void main(String[] args) throws IOException {
  16. BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  17. String line;
  18. while ((line = in.readLine()) != null) {
  19. int n = Integer.parseInt(line);
  20. int ret = new Solution().numSquares(n);
  21. String out = String.valueOf(ret);
  22. System.out.print(out);
  23. }
  24. }
  25. }

可以看到上边的逻辑,每次求 n 的时候都是创建新的对象然后调用方法。

这样会带来一个问题,假如第一次我们求了 90 的平方数和的最小个数,期间 dp 会求出 189 的所有的平方数和的最小个数。

第二次如果我们求 50 的平方数和的最小个数,其实第一次我们已经求过了,但实际上我们依旧会求一遍 150 的所有平方数和的最小个数。

我们可以通过声明 dpstatic 变量,这样每次调用就不会重复计算了。所有对象将共享 dp

  1. static ArrayList<Integer> dp = new ArrayList<>();
  2. public int numSquares(int n) {
  3. //第一次进入将 0 加入
  4. if(dp.size() == 0){
  5. dp.add(0);
  6. }
  7. //之前是否计算过 n
  8. if(dp.size() <= n){
  9. //接着之前最后一个值开始计算
  10. for (int i = dp.size(); i <= n; i++) {
  11. int min = Integer.MAX_VALUE;
  12. for (int j = 1; j * j <= i; j++) {
  13. min = Math.min(min, dp.get(i - j * j) + 1);
  14. }
  15. dp.add(min);
  16. }
  17. }
  18. return dp.get(n);
  19. }

解法三 BFS

参考 这里)。

相对于解法一的 DFS ,当然也可以使用 BFS

DFS 是一直做减法,然后一直减一直减,直到减到 0 算作找到一个解。属于一个解一个解的寻找。

BFS 的话,我们可以一层一层的算。第一层依次减去一个平方数得到第二层,第二层依次减去一个平方数得到第三层。直到某一层出现了 0,此时的层数就是我们要找到平方数和的最小个数。

举个例子,n = 12,每层的话每个节点依次减 1, 4, 9...。如下图,灰色表示当前层重复的节点,不需要处理。

279. Perfect Squares - 图3

如上图,当出现 0 的时候遍历就可以停止,此时是第 3 层(从 0 计数),所以最终答案就是 3

实现的话当然离不开队列,此外我们需要一个 set 来记录重复的解。

  1. public int numSquares(int n) {
  2. Queue<Integer> queue = new LinkedList<>();
  3. HashSet<Integer> visited = new HashSet<>();
  4. int level = 0;
  5. queue.add(n);
  6. while (!queue.isEmpty()) {
  7. int size = queue.size();
  8. level++; // 开始生成下一层
  9. for (int i = 0; i < size; i++) {
  10. int cur = queue.poll();
  11. //依次减 1, 4, 9... 生成下一层的节点
  12. for (int j = 1; j * j <= cur; j++) {
  13. int next = cur - j * j;
  14. if (next == 0) {
  15. return level;
  16. }
  17. if (!visited.contains(next)) {
  18. queue.offer(next);
  19. visited.add(next);
  20. }
  21. }
  22. }
  23. }
  24. return -1;
  25. }

解法四 数学

参考 这里)。

这个解法就不是编程的思想了,需要一些预备的数学知识。

四平方和定理),意思是任何正整数都能表示成四个平方数的和。少于四个平方数的,像 12 这种,可以补一个 0 也可以看成四个平方数,12 = 4 + 4 + 4 + 0。知道了这个定理,对于题目要找的解,其实只可能是 1, 2, 3, 4 其中某个数。

Legendre’s three-square theorem ,这个定理表明,如果正整数 n 被表示为三个平方数的和,那么 n 不等于

279. Perfect Squares - 图4

ab 都是非负整数。

换言之,如果

279. Perfect Squares - 图5

,那么他一定不能表示为三个平方数的和,同时也说明不能表示为一个、两个平方数的和,因为如果能表示为两个平方数的和,那么补个 0,就能凑成三个平方数的和了。

一个、两个、三个都排除了,所以如果

279. Perfect Squares - 图6

,那么 n 只能表示成四个平方数的和了。

所以代码的话,我们采取排除的方法。

首先考虑答案是不是 1,也就是判断当前数是不是一个平方数。

然后考虑答案是不是 4,也就是判断 n 是不是等于

279. Perfect Squares - 图7

然后考虑答案是不是 2,当前数依次减去一个平方数,判断得到的差是不是平方数。

以上情况都排除的话,答案就是 3

  1. public int numSquares(int n) {
  2. //判断是否是 1
  3. if (isSquare(n)) {
  4. return 1;
  5. }
  6. //判断是否是 4
  7. int temp = n;
  8. while (temp % 4 == 0) {
  9. temp /= 4;
  10. }
  11. if (temp % 8 == 7) {
  12. return 4;
  13. }
  14. //判断是否是 2
  15. for (int i = 1; i * i < n; i++) {
  16. if (isSquare(n - i * i)) {
  17. return 2;
  18. }
  19. }
  20. return 3;
  21. }
  22. //判断是否是平方数
  23. private boolean isSquare(int n) {
  24. int sqrt = (int) Math.sqrt(n);
  25. return sqrt * sqrt == n;
  26. }

解法一和解法二的话算比较常规的思想,我觉得可以看做暴力的思想,是最直接的思路。

解法三的话,只是改变了遍历的方式,本质上和解法一还是一致的。

解法四就需要数学功底了,这里仅做了解,记住结论就可以了。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情