动态规划完全背包问题06:完全平方数

219 阅读4分钟

完全平方数

力扣279. 完全平方数 - 力扣(LeetCode)
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:

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

示例 2:

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

可能刚看这种题感觉没啥思路,⼜平⽅和的,⼜最⼩数的。
我来把题⽬翻译⼀下:完全平⽅数就是物品(1,4,9,...可以⽆限件使⽤),凑个正整数n就是背包,问凑满这个背包最少有多少物品?感受出来了没,这么浓厚的完全背包氛围,⽽且和上一篇文章动态规划完全背包问题05:再兑换⼀次零钱! - 掘金 (juejin.cn)就是⼀样⼀样的。

动规五部曲:

1. 确定dp数组以及下标的含义

dp[i]:和为i的完全平⽅数的最少数量为dp[i]

2. 确定递推公式

仔细想想平方数其实就为i * i(i为正整数)dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。此时我们要选择最⼩的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);

3. dp数组如何初始化

dp[0]表示和为0的完全平⽅数的最⼩数量,那么dp[0]⼀定是0。有朋友问那0 * 0 也算是⼀种啊,为啥dp[0] 就是 0呢?
看题⽬描述,找到若⼲个完全平⽅数(⽐如 1, 4, 9, 16, ...),题⽬描述中可没说要从0开始,dp[0]=0完全是为了递推公式。
⾮0下标的dp[j]应该是多少呢?从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最⼩的,所以⾮0下标的dp[i]⼀定要初始为最⼤值可以是Integer.MAX_VALUE也可以是n + 1至于为什么我上一篇文章已经讲过了就不多提了,这样dp[j]在递推的时候才不会被初始值覆盖。

4. 确定遍历顺序

我们知道这是完全背包,如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。在动态规划完全背包问题05:再兑换⼀次零钱! - 掘金 (juejin.cn)中我们就深⼊探讨了这个问题,本题也是⼀样的,是求最⼩数!所以本题外层for遍历背包,⾥层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的! 这⾥给出外层遍历背包,⾥层遍历物品的代码:

         for(int i = 1; i <= n; i++){
            dp[i] = n + 1;
            for(int j = 1; j * j <= i; j++){
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }

5. 举例推导dp数组

image.png
分析完毕Java代码如下:

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for(int i = 1; i <= n; i++){
            dp[i] = n + 1;
            for(int j = 1; j * j <= i; j++){
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }
}

文章到这里就结束了吗?哈哈其实不!偷偷告诉大家一个小技巧可以减少题目的执行用时。首先我们先看一下上面代码的运行用时: image.png
再看一下简单小优化后的代码运行用时: image.png
有没有感到很惊讶在内存消耗差不多的情况下足足提高了一倍多的效率!
废话不多说直接上优化后的代码:

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for(int i = 1; i <= n; i++){
            int min = n + 1;
            for(int j = 1; j * j <= i; j++){
                min = Math.min(min, dp[i - j * j]);
            }
            dp[i] = min + 1;
        }
        return dp[n];
    }
}

可能聪明的朋友一眼就看出来秘诀就是减少对数组数据的查询和更改!我们定义一个中间变量来代替dp[i]在内层循环中的数据查询和数据更改。

文章参考:代码随想录