Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)

Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)

动态规划应该如何学习?-CSDN博客

01背包模板 | 学习总结-CSDN博客

完全背包模板总结-CSDN博客

难点:

代码都不难写,如何想到完全背包并把具体问题抽象为完全背包才是关键

文章目录

377.组合总和IV

377. 组合总和 Ⅳ - 力扣(LeetCode)

思路分析:

虽然说是组合,但本质是求的排列,要考虑元素的顺序

代码随想录只是说了一下遍历顺序不同,可以分别求出排列数量和组合数量,但大家肯定还是不太清楚。还是看看灵神的题解怎么说吧。

本题其实就是 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

相关推荐
Swift社区5 分钟前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman37 分钟前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
IT 青年1 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Yang.991 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王1 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_1 小时前
C++自己写类 和 运算符重载函数
c++
Dong雨1 小时前
力扣hot100-->栈/单调栈
算法·leetcode·职场和发展
六月的翅膀1 小时前
C++:实例访问静态成员函数和类访问静态成员函数有什么区别
开发语言·c++
SoraLuna2 小时前
「Mac玩转仓颉内测版24」基础篇4 - 浮点类型详解
开发语言·算法·macos·cangjie