Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)
难点:
代码都不难写,如何想到完全背包并把具体问题抽象为完全背包才是关键
文章目录
- [Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)](#Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯))
377.组合总和IV
思路分析:
虽然说是组合,但本质是求的排列,要考虑元素的顺序
代码随想录只是说了一下遍历顺序不同,可以分别求出排列数量和组合数量,但大家肯定还是不太清楚。还是看看灵神的题解怎么说吧。
本题其实就是 70. 爬楼梯,我们每次从 nums 中选一个数,作为往上爬的台阶数,问爬 target 个台阶有多少种方案。70 那题可以看作 nums=[1,2],因为每次只能爬 1 个或 2 个台阶。
dp[i]的含义就是爬上第i个台阶的方案数量
1.在那道题中
我们的代码是
cpp
dp[i]=dp[i-1]+dp[i-2]
2.如果说我们一次可以爬k个台阶,当然k要比target(要爬的总楼梯数量)小
cpp
dp[i]=dp[i-1]+dp[i-2]+dp[i-3]+....+dp[i-k]
//等价于
for(int j=1;j<=k;j++)
dp[i]+=dp[i-j];
3.如果说我们一次可以爬的台阶数量是nums数组里面的,那么j就是nums[j],上面的1-k就相当于这里的nums数组的遍历
cpp
dp[i]=dp[i-nums[1]]+dp[i-nums[2]]+dp[i-nums[3]]+....+dp[i-nums[nums.size()-1]]
//等价于
for(int j=0;j<nums.size();j++)
dp[i]+=dp[i-nums[j]];
相当于,在第一种情况中
nums数组为{1,2}
在第二种情况中
nums数组为{1,2,3,...,k}
难点讲解
1.为什么这样可以求出来排列数量?
举个例子,我们要登上台阶3(target=3)
我们的nums数组为{1,2}
那很显然我们dp[3]=dp[1]+dp[2]了
就是从第一个台阶一次爬2个(1,2先爬一个再爬两个)
和从第二个台阶一次爬1个(2,1先爬两个再爬一个)
很明显,可以是排列的原因就是,我们在爬每一个台阶往上爬看能不能看target的时候会从nums的数组的第一个元素开始重新遍历,会把所有的元素重新遍历一遍
2.dp数组如何初始化,我们应该如何理解初始化
cpp
dp[0]=1;
要知道这道题是达到target就行
假如我们nums里面有一个值就是target
那岂不是直接就会有一种方案是从0到target
而我们的递推公式里面有这个情况
dp[target]=...+dp[target-target](这个值就是dp[0])
这个方案数量为1,除此之外呢不会有其他的情况可以取到这个值
这个初始化和下面代码等价
cpp
for(int i=0;i<nums.size();i++)
if(nums[i]<=target)
dp[nums[i]]=1;
为什么是等价的呢?
如果dp[0]没有初始化为1
那dp[nums[i]]=...+dp[nums[i]-nums[i]]里面的最后一项就是dp[0]=0,你发现就少加了一个1
而我们初始化的时候上面的这三行代码已经把它初始化成1了,dp[0]=0也就无所谓了
所以两种初始化方式都可以
而用回溯法递归的话,dp[0]=1可以理解为是我们从target开始向下爬,可以爬到0,那就是找到了一个合法的方案,我们就会返回1
1.动态规划
既然递推公式和初始化全想清楚了,那就直接上动态规划,完了再想回溯和记忆化搜索
1.确定dp数组以及下标的含义
dp[i]: 凑成目标正整数为i的排列个数为dp[i]
也就是上面说的爬到第i个台阶有多少种方法
2.确定递推公式
正如上面爬楼梯所说的
cpp
for(int j=0;j<nums.size();j++)
dp[i]+=dp[i-nums[j]];
3.dp数组如何初始化
请看难点讲解2
4.确定遍历顺序
外层循环遍历台阶i
内层循环nums数组
都是从前往后遍历
完整代码:
啊,i>=nums[j]是因为你一次爬(nums[j])的总不能比要爬的总数(i)高了吧
cpp
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned> dp(target+1,0);
/*for(int i=0;i<nums.size();i++)
if(nums[i]<=target)
dp[nums[i]]=1;*/
dp[0]=1;
for(int i=1;i<=target;i++)
for(int j=0;j<nums.size();j++)
if(i>=nums[j])
dp[i]+=dp[i-nums[j]];
return dp[target];
}
};
2.回溯法
我们从第c个台阶往第0个台阶走
能到达第0个台阶那就是找到了一个合法的方案
到不了就是没找到一个合法的方案
1.参数和返回值
c是要爬的第几个台阶
nums是我们一次可以爬几个台阶
dfs返回的就是我们爬到c可以有几个方案
cpp
int dfs(int c,vector<int>& nums)
2.终止条件
如果c==0,说明我们可以从target向下走到底
说明找到了一个合法的方法,返回1
cpp
if(c==0)
return 1;
3.本层逻辑
动态规划两层for循环,那么在递归函数里面就需要一个for循环,另外一层循环由递归体现
还是一样的,爬到c的方法就是,从c-num[0],c-nums[1]...c-nums[i]这些台阶爬到c的方案数量相加
cpp
int res=0;
for(int i=0;i<nums.size();i++)
if(c>=nums[i])
res+=dfs(c-nums[i],nums);
return res;
完整代码:
当然是超时的
cpp
class Solution {
public:
int dfs(int c,vector<int>& nums)
{
if(c==0)
return 1;
int res=0;
for(int i=0;i<nums.size();i++)
if(c>=nums[i])
res+=dfs(c-nums[i],nums);
return res;
}
int combinationSum4(vector<int>& nums, int target) {
return dfs(target,nums);
}
};
3.记忆化搜索
就是还是全都初始化为-1,每次返回前给dp赋值,碰到不是-1的那就是算过的,那就直接返回计算过的结果,不需要再次递归了
完整代码:
cpp
class Solution {
public:
int dfs(int c,vector<int>& nums,vector<unsigned>& dp)
{
if(c==0)
return 1;
int res=0;
if(dp[c]!=-1)
return dp[c];
for(int i=0;i<nums.size();i++)
if(c>=nums[i])
res+=dfs(c-nums[i],nums,dp);
return dp[c]=res;
}
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned> dp(target+1,-1);
return dfs(target,nums,dp);
}
};
到这里我们会发现,这里的dp不就是先遍历背包容量后遍历物品的完全背包吗?
所以先遍历物品,后遍历背包容量 得到的就是nums能凑成target的组合
先遍历背包容量,后遍历物品 得到的就是nums能凑成target的排列
组合
经典题目零钱兑换II
Day39 | 动态规划 :完全背包应用 零钱兑换&&零钱兑换II-CSDN博客
排列
本次题解的组合总和IV