Loading

Medium | LeetCode 279. 完全平方数 | 动态规划 | BFS

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

解题思路

方法一:暴力枚举法

先转化问题:给定一个完全平方数列表和正整数N, 求出完全平方数组合成N的组合, 要求组合中的解拥有完全平方数的最小个数。

采用动态规划的方法求解。这种方法本质上是暴力枚举所有的组合。

状态转移方程如下:

\[\text { numSquares }(n)=\min (\text { numSquares }(\mathbf{n}-\mathbf{k})+1) \quad \forall k \in square \space numbers \]

这里采用递归的方式实现动态规划。

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

把方法三的思路, 描绘成 一棵树, 然后通过遍历的方式, 找到叶节点是完全平方数的最小深度。

  1. 首先,我们准备小于给定数字 n 的完全平方数列表(即 square_nums)。
  2. 然后创建 queue 遍历,该变量将保存所有剩余项在每个级别的枚举。
  3. 在主循环中,我们迭代 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;
}
posted @ 2021-01-31 11:20  反身而诚、  阅读(198)  评论(0编辑  收藏  举报