⭐️前面的话⭐️
本篇文章将介绍动态规划中的背包问题——完全背包问题,前面我们已经介绍了什么是完全背包问题以及对应的解决方案,本文将列举一道实际问题来强化对完全背包的解题以及优化思维。
📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创,CSDN首发!
📆首发时间:🌴2022年10月27日🌴
✉️坚持和努力一定能换来诗与远方!
💭推荐书籍:📚《算法》,📚《算法导论》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
279. 完全平方数
难度中等
给你一个整数 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
的完全平方数中选择若干数凑成n
,求凑成数n
的最小数量。
那这道题就是一道完全背包问题,即背包中的【物品】就是不大于n
的完全平方数,每件物品的价值可以理解为1
,每件物品体积可以理解为数字的值,每个物品可以选择无数次 ,求凑成数n
的所选择物品的最小数量(即凑出总体积恰好为n
时的物品最低价值)。
状态定义: 不妨定义f[i][j]f[i][j]f[i][j]表示从前iii件物品中选择,恰好凑成jjj,所选物品的最小数量,任何数都是1
的倍数,所以不存在无法凑出的情况。
确定初始状态: 当i=0i=0i=0时,只能选择物品【1】,因为111可以被任何正整数整除,所以f[0][j]=j/1f[0][j]=j/1f[0][j]=j/1。
状态转移方程推导: 当我们选择第iii件物品的时候,我们可以选择0,1,2...k0,1,2...k0,1,2...k件,其中kkk是最大能够选择的件数,即在恰好凑出jjj的情况下。
假设第iii件物品的数值【体积】为ttt。
当我们不选择第iii件物品时,即k=0k=0k=0,f[i][j]=f[i−1][j]f[i][j]=f[i-1][j]f[i][j]=f[i−1][j]。
当我们选择111件第iii件物品时,即k=1k=1k=1,f[i][j]=f[i−1][j−t]+1f[i][j]=f[i-1][j - t] + 1f[i][j]=f[i−1][j−t]+1。
当我们选择222件第iii件物品时,即k=2k=2k=2,f[i][j]=f[i−1][j−2∗t]+2f[i][j]=f[i-1][j - 2 * t] + 2f[i][j]=f[i−1][j−2∗t]+2。
…
当我们选择kkk件第iii件物品时,即k=kk=kk=k,f[i][j]=f[i−1][j−k∗t]+kf[i][j]=f[i-1][j - k * t]+kf[i][j]=f[i−1][j−k∗t]+k。
我们所要求的是最小平方数的数量值,所以取以上所有情况的最小值即可。
综上所述,我们的状态转移方程就出来了,不妨记当前物品的价值为:
f[i][j]=max(f[i−1][j−k∗t]+k),k=0,1,...f[i][j]=max(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(n2∗len)O(n^2*len)O(n2∗len),kkk的值不会大于nnn,因为ttt最小为111,最多选择的数的数量不会超过nnn。
空间复杂度:O(n∗len)O(n*len)O(n∗len)
根据观察我们知道第iii行的状态仅依赖与第i−1i-1i−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)。
时间复杂度:O(n2∗len)O(n^2*len)O(n2∗len),kkk的值不会大于nnn,因为ttt最小为111,最多选择的数的数量不会超过nnn。
空间复杂度:O(n)O(n)O(n)
首先我们对状态转移方程进行一个简单的推导:
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]=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]=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]=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)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) 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<=jk*t<=jk∗t<=j。
通过观察上面两个状态的式子,我们发现后面一部分式子是差了一个111如下图:
所以我们可以进一步优化状态转移方程,即:
f[i][j]=min(f[i−1][j],f[i][j−t]+1)f[i][j]=min(f[i-1][j],f[i][j-t]+1) f[i][j]=min(f[i−1][j],f[i][j−t]+1)
对于新推导出来的状态转移方程,它的状态转移仅仅只依赖与上一行同列状态与同一行元素前面的元素,所以我们可以将原来的二维数组优化为一维,由于它只依赖左边与正上方的元素,我们可以采取从小到大遍历背包容量状态来求背包中所放物品最大值。
只保留【凑出数字值】维度,状态转移方程为:
f[j]=max(f[j],f[j−t]+1)f[j]=max(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(len∗n)O(len*n)O(len∗n)
空间复杂度:O(n+1)O(n+1)O(n+1)
本篇文章介绍了【完全背包】的运用,使用该模型运用到实际的问题当中,强化一维优化过程的推导。
对于完全背包的优化,相比于0-1背包问题的优化,形式上,我们只需要将 01 背包问题的「一维空间优化」解法中的「容量维度」遍历方向从「从大到小 改为 从小到大」就可以解决完全背包问题。
但本质是因为两者进行状态转移时依赖了不同的格子:
0 -1 背包依赖的是「上一行正上方的格子」和「上一行左边的格子」。
完全背包依赖的是「上一行正上方的格子」和「本行左边的格子」。
参考资料: 宫水三叶背包问题