279. 完全平方数 (perfect squares)

3,994 阅读3分钟

"又见背包,让我走得不再缓慢"

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

279. 完全平方数 题目描述:给你一个整数 n ,返回和为 n 的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,149 和 16 都是完全平方数,而 3 和 11 不是。

示例1示例2示例3示例4
输入n=12n = 12
输出33
解释12=4+4+412 = 4 + 4 + 4
输入n=13n = 13
输出22
解释13=4+913 = 4 + 9
输入n=3n = 3
输出33
解释3=1+1+13 = 1 + 1 + 1
输入n=1n = 1
输出11
解释1=11 = 1

中规中矩的动态规划

问题抽象:有一个容量为 nn 的背包,我们要从数列 [1,2,3,...,floor(n)][1, 2, 3, ..., floor(\sqrt n)] 中选择元素(可以重复),元素平方和等于 nn 时所用元素个数最少为多少个?这道题几乎与 # 322. 零钱兑换 一样,只有把硬币面值改成了数列 [1,2,3,...,floor(n)][1, 2, 3, ..., floor(\sqrt n)],元素和等于 nn 改成了元素平方和等于 nn。所以该问题即为 完全背包组合问题

解释 floor(n)floor(\sqrt n):因为容量最大是 nn,所以数列中最大元素 mm 的平方一定存在: m2nm^2 \le n,故 mnm \le \sqrt n,对最大元素向 00 方向取整,即为 floor(n)floor(\sqrt n)

1、确定 dp 状态数组

dp[i]dp[i] 是总数为 ii 时所需要的最少完全平方数的个数,其中 i[0,n]i \in [0,n]

2、确定 dp 状态方程

状态转移方程与# 322. 零钱兑换完全一样。

dp[i]=min(dp[iitem]itemitems)+1dp[i] = min(dp[i-item] | item \in items) + 1

其中,itemsitems 是由完全平方数组合的数组,即 [12,22,32,...,(floor(n))2][1^2, 2^2, 3^2, ..., (floor(\sqrt n))^2]

3、确定 dp 初始状态

dp[0]dp[0] 是总数为 00 时所需要的最少完全平方数的个数,这个值肯定为 00,即 dp[0]=0dp[0] = 0(这是一个非法值,题设已经给出了 n1n \ge 1)。

dp[1]dp[1] 是总数为 11 时所需要的最少完全平方数的个数,只能由 11 组成,即 dp[1]=1dp[1] = 1

同理,可以推算出 dp[2]=2dp[3]=3dp[2] = 2、dp[3] = 3

因为要求最小值,我们可以用一个局部相对较大的值来初始化。对于正整数 mm 最多需要多少个完全平方数呢?一定是 mm 个(由 mm11 组成),所以这个局部相对较大值一定要大于 mm,所以可以取 m+1m + 1。(当然取 infinityinfinity 也是完全可以的。

4、确定遍历顺序

完全背包组合问题,可以先遍历背包,再遍历物品

  • 外层循环从 i=4i = 4i=ni = n;

  • 内层循环从 i=0i = 0j=items.length1j = items.length - 1

5、确定最终返回值

首先我们确定一个正整数一定能编凑成若干个完全平方数的平方之和,所以无需额外判断,直接返回 dp[n]dp[n] 即可。

6、代码示例

/**
 * 空间复杂度 O(n * n^0.5)
 * 时间复杂度 O(n * n^0.5)
 */
function numSquares(n: number): number {
    const length = ~~Math.sqrt(n); // ~~ 向 0 方向取整
    const items = Array.from({ length }, (_, index) => (index + 1) ** 2);
    const dp = new Array(n + 1).fill(n + 1);
    dp[0] = 0;
    dp[1] = 1;
    dp[2] = 2;
    dp[3] = 3;

    for(let i = 4; i <= n; i++) {
        for (let j = 0; j < length; j++) {
            if (i >= items[j]) {
                dp[i] = Math.min(dp[i], dp[i - items[j]] + 1);
            }
        }
    }

    return dp[n];
};

参考

# 重识动态规划

# 重识背包问题(上)

# 重识背包问题(下)