完全平方数

263 阅读1分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情

给你一个整数 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 的组合。

那么我们可以说我们找到了最小的组合,因为我们贪心的从小到大的枚举组合。

为了更好的解释,我们首先定义一个名为 is_divided_by(n, count) 的函数,该函数返回一个布尔值,表示数字 n是否可以被一个数字 count 组合,而不是像前面函数 numSquares(n) 返回组合的确切大小。

与递归函数 numSquare(n) 不同,is_divided_by(n, count) 的递归过程可以归结为底部情况(即 count==1)更快。

下面是一个关于函数 is_divided_by(n, count) 的例子,它对 输入 n=5count=2 进行了分解。

通过这种重新构造的技巧,我们可以显著降低堆栈溢出的风险。

算法

  • 首先,我们准备一个小于给定数字 n 的完全平方数列表(称为 square_nums)。
  • 在主循环中,将组合的大小(称为 count)从 1 迭代到 n,我们检查数字 n 是否可以除以组合的和,即 is_divided_by(n, count)
  • 函数 is_divided_by(n, count) 可以用递归的形式实现,汝上面所说。
  • 在最下面的例子中,我们有 count==1,我们只需检查数字 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% 的提交要快。

private:
    static bool unfilled;
    static vector<int> dp;
    static void fill() {
        dp[1] = 1;
        for (int i = 2; i < 10001; ++i) {
            int min_ = INT_MAX;
            for (int j = 1; j * j <= i; ++j) {
                min_ = min(min_, dp[i - j*j]);
            }
            dp[i] = 1 + min_;
        }
        unfilled = false;
    }
public:
    int numSquares(int n) {
        if (unfilled)
            fill();
        return dp[n];
    }
};
bool Solution::unfilled = true;
vector<int> Solution::dp = vector<int> (10001, 0);