题目来源:(力扣377)
https://leetcode.cn/problems/combination-sum-iv/
题干:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
说直白一点什么是完全背包:就是有一堆物品,每个物品有自己的价值和重量,现在手上有一个容重有上限的背包,让你去那一堆物品当中去挑着装,问怎么装能装出最大价值呢?如果每个物品只能用一次,那就是典型的0-1背包问题,而如果每个物品都能使用无数次,那就会转变为完全背包问题
通常维护一个二维数组dp [ i ] [ j ] :含义就是:前i个字符,在当前背包容量为j的情况下,所能得到的最大价值就是dp [ i ] [ j ]
那根据dp数组的定义,我们可以比较轻松的得到递推公式:
//1.如果当前背包装的下:
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]+value[i]])//注意你要使用的i是下标还是就是1代表第一个物品,所以细节注意一下
//2.如果当前背包压根装不下,即j<weight[i]
dp[i][j] = dp[i-1][j]
而一般题目都会要求我们去返回一个dp [n] [target],n就是给了一个数组的长度,也就是说将整个数组都作为考虑的对象,target表示一个目的,比如能否等和的分割出一个子集啊那样的题,target就是所有元素的和除2得到,如果能在所给数组中挑选出某些个元素填满容量为target的背包,题目就可以返回true咯,很多题目都是0-1背包的抽象化.但是思路都差不多是这个.
但是0-1背包我认为最应该注意的地方:就是两层for循环遍历的顺序上,它有时候是先物品再背包,有时候只能反过来,有时候又都可以,那我发现:只有你预想的递推公式是依赖于[i-1]这样的前面的数据,那一般就都是先物品再背包,因为一行一行填二维数组时,你需要先拿到左上角的数据才能推出当前位置该填什么.就比如换零钱的问题,要用背包问题解决的话,背包的容量就是你想换的钱的金额,待选的物品就是那个数组,如果每个面额的钱只能有一次就是0-1背包,反之就是完全背包问题.然后填当前位置的时候,穷举出它所能依赖的前面你已经算出来的数据的所有情况,并依赖于它们给出你当前位置的值是多少,所以我认为对局部的穷举就是递推公式出来的关键,当然这只是个人总结啦,大家刷题可以试试这个思维,我怎么完全的考虑已经算出来的数据推出当前位置的数据是多少?就是这样
如果递推公式显示当前位置的数据只和它相邻的格格内的数据相关,那此时大概率是可以压缩的,怎么压缩呢?其实双层for循环的遍历顺序已经给了我们答案,比如先遍历i(行)再遍历j(列),也就是说你当前在填第i行的第j列的数据:dp[i] [j],如递推公式就拿上面的那个为例,其实你上一层外层的for循环给的第j列的数据和你当前i层遍历到的第j列数据都在同一层对吧,如果你讲i这个维度拍扁,你如果拿一个dp[j],啥都不干,其实你是拿到了二维数组中的dp[i-1] [j]的数据,所以:
dp[j] = Math.max(dp[j],dp[j-weight[i]+value[i]])
此时就会成功优化空间复杂度啦~
当然如果是什么倒着遍历,并且当前位置和已经算出来的三个位置的数据相关,可能还需要搭配一定的变量去记录某个数据,因为你不记录,就会被覆盖,那就再也拿不到了,比如你可以试试最长回文子序列的空间如何压缩,此处不再赘述
刚才也说到了,优化成一维的因为要拿老数据用,所以如果你内层for循环还是正序遍历的话,就会把老数据直接覆盖了,那你之后的位置还想拿老数据出来用,你就拿不到了,所以此时就需要将内层for循环变成倒叙遍历,正是因为二维数组一个位置填好了之后不会再被修改,所以你内层for循环即使是正序遍历也没事.
因为本题如果仅仅是在0-1背包的基础之上考虑物品可被复用的话,就顶多上述提及的代码中将选择列表多加一个已选的物品即可完成物品复用.
但是不知道你注意过没用,如果先遍历物品再遍历背包,其实:我们假设物品有两个:1,2分别表示第一个和第二个物品的重量(也是价值)
那先遍历物品再遍历背包的话:每个背包里就一定是先1后2,而不会出现先2后1的情况,这就说明了:
先物品后背包的遍历顺序将给我们带来组合的结果
反过来呢?
先背包再物品,比如我们考虑一个包装满有几种方案可以吧,就设容量是3,有物品1,2
那这个包装满的方案就会包含1,2和2,1,虽然物品还是这两个物品,但却算作了两个不同的结果,所以:
先背包再物品的遍历顺序讲给我们带来排列的结果
那回到这个题,该怎么写呢?因为经过上述分析,我们知道本题在求排列,并且背包容量是target,并且呢还是一个完全背包问题.
如果我们定义dp[i] [j]是前i个数组元素,能有最多dp[i] [j]中方案组成j
那:
//递推公式就是:dp[i][j] = dp[i-1][j] + dp[i][j-1]
但是这样真的对吗?答案是否定的.
此时我们应当这样去定义dp[i] [j] :组合长度为i的所有方案,其中有多少种能凑出j
所以
所以可以直接上代码啦:
class Solution {
public int combinationSum4(int[] nums, int target) {
//dp[i][j]定义为组合长度为i的所有方案中有多少种能组成j
int[][] dp = new int[target+1][target+1];//想返回dp[target][target]
dp[0][0] = 1;
int ans = 0;
for(int i =1;i<=target;i++){
for(int j = 0; j<=target;j++){
for(int tmp:nums){
if(j>=tmp) dp[i][j] +=dp[i-1][j-tmp];
}
}
ans+=dp[i][target];
}
return ans;
}
}
上述代码就可以考虑到1,2和2,1的不同,二者都可以为结果做贡献这样.
考虑如何优化为一维:
应当考虑针对每个背包容量下:1,2和2,1视作不同的组合
class Solution {
public int combinationSum4(int[] nums, int target) {
//int n = nums.length;
//dp[i][j]定义为组合长度为i的所有方案中有多少种能组成j
int[] dp = new int[target+1];//想返回dp[target]
dp[0] = 1;
for(int i =1;i<=target;i++){
for(int tmp:nums){
if(i>=tmp) dp[i]+=dp[i-tmp];
}
}
return dp[target];
}
}