【动态规划之完全背包问题】如何将完全背包运用到实际问题,强化完全背包以及一维优化的推导

举报
未见花闻 发表于 2022/11/30 21:25:44 2022/11/30
【摘要】 【动态规划之完全背包问题】如何将完全背包运用到实际问题,强化完全背包以及一维优化的推导

⭐️【强化完全背包练习】完全背包原题⭐️

🔐题目详情

279. 完全平方数

难度中等

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= n <= 104

💡解题思路与代码

🍭朴素解法

我们分析一下,本题的意思是从不大于n的完全平方数中选择若干数凑成n,求凑成数n的最小数量。

那这道题就是一道完全背包问题,即背包中的【物品】就是不大于n的完全平方数,每件物品的价值可以理解为1,每件物品体积可以理解为数字的值,每个物品可以选择无数次 ,求凑成数n的所选择物品的最小数量(即凑出总体积恰好为n时的物品最低价值)。

状态定义: 不妨定义 f [ i ] [ j ] f[i][j] 表示从前 i i 件物品中选择,恰好凑成 j j ,所选物品的最小数量,任何数都是1的倍数,所以不存在无法凑出的情况。

确定初始状态: i = 0 i=0 时,只能选择物品【1】,因为 1 1 可以被任何正整数整除,所以 f [ 0 ] [ j ] = j / 1 f[0][j]=j/1

状态转移方程推导: 当我们选择第 i i 件物品的时候,我们可以选择 0 , 1 , 2... k 0,1,2...k 件,其中 k k 是最大能够选择的件数,即在恰好凑出 j j 的情况下。
假设第 i i 件物品的数值【体积】为 t t
当我们不选择第 i i 件物品时,即 k = 0 k=0 f [ i ] [ j ] = f [ i 1 ] [ j ] f[i][j]=f[i-1][j]
当我们选择 1 1 件第 i i 件物品时,即 k = 1 k=1 f [ i ] [ j ] = f [ i 1 ] [ j t ] + 1 f[i][j]=f[i-1][j - t] + 1
当我们选择 2 2 件第 i i 件物品时,即 k = 2 k=2 f [ i ] [ j ] = f [ i 1 ] [ j 2 t ] + 2 f[i][j]=f[i-1][j - 2 * t] + 2

当我们选择 k k 件第 i i 件物品时,即 k = k k=k f [ i ] [ j ] = f [ i 1 ] [ j k t ] + k f[i][j]=f[i-1][j - k * t]+k

我们所要求的是最小平方数的数量值,所以取以上所有情况的最小值即可。

综上所述,我们的状态转移方程就出来了,不妨记当前物品的价值为:

f [ i ] [ j ] = m a x ( f [ i 1 ] [ j k t ] + k ) , k = 0 , 1 , . . . f[i][j]=max(f[i-1][j-k*t]+k),k=0,1,...

实现代码:

class Solution {
    public int numSquares(int n) {
        //动态规划 完全背包问题
        //1.枚举1-n之间所有的完全平方数
        int len = (int) Math.sqrt(n);
        int[] nums = new int[n];
        for (int i = 1; i * i <= n; i++) {
            nums[i - 1] = i * i;
        }

        //状态定义f[i][j]表示和为n完全平方和的最小数量

        int[][] f = new int[len][n + 1];

        //确定初始状态,处理只有一个数字时
        for (int j = 1; j <= n; j++) {
            int k = j / nums[0];
            //由于第一个数字是1 大于1的整数一定能够被1整除,所以判断是否为1的倍数可以省略
            f[0][j] = k;
        }

        //状态转移
        for (int i = 1; i < len; i++) {
            int t = nums[i];
            for (int j = 1; j <= n; j++) {
                //不选
                f[i][j] = f[i - 1][j];
                //选择k个
                for (int k = 1; k * t <= j; k++) {
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - k * t] + k);
                }
            }
        }
        return f[len - 1][n];
    }
}

时间复杂度: O ( n 2 l e n ) O(n^2*len) k k 的值不会大于 n n ,因为 t t 最小为 1 1 ,最多选择的数的数量不会超过 n n
空间复杂度: O ( n l e n ) O(n*len)

🍭滚动数组优化空间

根据观察我们知道第 i i 行的状态仅依赖与第 i 1 i-1 行的状态,因此我们可以使用滚动数组进行优化。

实现代码:

class Solution {
    public int numSquares(int n) {
        //动态规划 完全背包问题
        //1.枚举1-n之间所有的完全平方数
        int len = (int) Math.sqrt(n);
        int[] nums = new int[n];
        for (int i = 1; i * i <= n; i++) {
            nums[i - 1] = i * i;
        }

        //状态定义f[i][j]表示和为n完全平方和的最小数量

        int[][] f = new int[2][n + 1];

        //确定初始状态,处理只有一个数字时
        for (int j = 1; j <= n; j++) {
            int k = j / nums[0];
            //由于第一个数字是1 大于1的整数一定能够被1整除,所以判断是否为1的倍数可以省略
            f[0][j] = k;
        }

        //状态转移
        for (int i = 1; i < len; i++) {
            int t = nums[i];
            int cur = i & 1;
            int pre = (i - 1) & 1;
            for (int j = 1; j <= n; j++) {
                //不选
                f[cur][j] = f[pre][j];
                //选择k个
                for (int k = 1; k * t <= j; k++) {
                    f[cur][j] = Math.min(f[cur][j], f[pre][j - k * t] + k);
                }
            }
        }
        return f[(len - 1) & 1][n];
    }
}

对于时空复杂度,只是优化了空间而已,所以时间复杂度不发生改变,空间复杂度优化到 O ( n ) O(n)
时间复杂度: O ( n 2 l e n ) O(n^2*len) k k 的值不会大于 n n ,因为 t t 最小为 1 1 ,最多选择的数的数量不会超过 n n
空间复杂度: O ( n ) O(n)

🍭一维数组优化空间

首先我们对状态转移方程进行一个简单的推导:

f [ i ] [ j ] = m i n ( f [ i 1 ] [ j ] , f [ i 1 ] [ j t ] + 1 , f [ i 1 ] [ j 2 t ] + 2 , . . . , f [ i 1 ] [ j k t ] + k ) f[i][j]=min(f[i-1][j],f[i-1][j-t]+1,f[i-1][j-2*t]+2,...,f[i-1][j-kt]+k)

f [ i ] [ j t ] = m i n ( f [ i 1 ] [ j t ] , f [ i 1 ] [ j 2 t ] + 1 , f [ i 1 ] [ j 3 t ] + 2... , f [ i 1 ] [ j k t ] + k 1 ) f[i][j-t]=min(f[i-1][j-t],f[i-1][j-2t]+1,f[i-1][j-3t]+2...,f[i-1][j-kt]+k-1)

其中 k t < = j k*t<=j

通过观察上面两个状态的式子,我们发现后面一部分式子是差了一个 1 1 如下图:

1

所以我们可以进一步优化状态转移方程,即:

f [ i ] [ j ] = m i n ( f [ i 1 ] [ j ] , f [ i ] [ j t ] + 1 ) f[i][j]=min(f[i-1][j],f[i][j-t]+1)

对于新推导出来的状态转移方程,它的状态转移仅仅只依赖与上一行同列状态与同一行元素前面的元素,所以我们可以将原来的二维数组优化为一维,由于它只依赖左边与正上方的元素,我们可以采取从小到大遍历背包容量状态来求背包中所放物品最大值。

1

只保留【凑出数字值】维度,状态转移方程为:

f [ j ] = m a x ( f [ j ] , f [ j t ] + 1 ) f[j]=max(f[j],f[j-t]+1)

实现代码:

class Solution {
    public int numSquares(int n) {
        //动态规划 完全背包问题
        //1.枚举1-n之间所有的完全平方数
        int len = (int) Math.sqrt(n);
        int[] nums = new int[n];
        for (int i = 1; i * i <= n; i++) {
            nums[i - 1] = i * i;
        }

        //状态定义f[j]表示和为n完全平方和的最小数量

        int[] f = new int[n + 1];

        //确定初始状态,处理只有一个数字时
        for (int j = 1; j <= n; j++) {
            int k = j / nums[0];
            //由于第一个数字是1 大于1的整数一定能够被1整除,所以判断是否为1的倍数可以省略
            f[j] = k;
        }

        //状态转移
        for (int i = 1; i < len; i++) {
            int t = nums[i];
            for (int j = t; j <= n; j++) {
                //f[j]=min(f[j], f[j-t]+1)

                f[j] = Math.min(f[j], f[j - t] + 1);
            }
        }
        return f[n];
    }
}

时间复杂度: O ( l e n n ) O(len*n)
空间复杂度: O ( n + 1 ) O(n+1)

🌱总结

本篇文章介绍了【完全背包】的运用,使用该模型运用到实际的问题当中,强化一维优化过程的推导。

对于完全背包的优化,相比于0-1背包问题的优化,形式上,我们只需要将 01 背包问题的「一维空间优化」解法中的「容量维度」遍历方向从「从大到小 改为 从小到大」就可以解决完全背包问题。
但本质是因为两者进行状态转移时依赖了不同的格子:
0 -1 背包依赖的是「上一行正上方的格子」和「上一行左边的格子」。
完全背包依赖的是「上一行正上方的格子」和「本行左边的格子」。


参考资料: 宫水三叶背包问题

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。