leetcode_刷题总结(c++)_动态规划_背包类问题

主要参考博客:

一篇文章吃透背包问题!(细致引入+解题模板+例题分析+代码呈现)
动态规划之背包问题以及优化为一维数组和完全背包问题(最易理解的讲解)
动态规划:关于01背包问题,你该了解这些!(滚动数组)

背包定义

给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
注意:
1、背包容量target和物品nums的类型可能是数,也可能是字符串
2、target可能题目已经给出(显式),也可能是需要从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合

背包分类

常见的背包类型主要有以下几种:
1、0/1背包问题:每个元素最多选取一次
2、完全背包问题:每个元素可以重复选择
3、组合背包问题:背包中的物品要考虑顺序
4、分组背包问题:不止一个背包,需要遍历每个背包

问题分类

1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合

因此把背包类型和问题类型结合起来就会出现以下细分的题目类型:
1、0/1背包最值问题
2、0/1背包存在问题
3、0/1背包组合问题
4、完全背包最值问题
5、完全背包存在问题
6、完全背包组合问题
7、分组背包最值问题
8、分组背包存在问题
9、分组背包组合问题
这九类问题我认为几乎可以涵盖力扣上所有的背包问题

背包问题解题模板

二维dp模板

// 0-1背包问题模板代码(二维)
void bags(){
   
    vector<int> weight = {
   1, 3, 4};   //各个物品的重量
    vector<int> value = {
   15, 20, 30}; //对应的价值
    int bagWeight = 4;                //背包最大能放下多少重的物品

    // 二维数组:状态定义:dp[i][j]表示从0-i个物品中选择不超过j重量的物品的最大价值
    vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));

    // 初始化:第一列都是0,第一行表示只选取0号物品最大价值
    for(int j=bagWeight; j>=weight[0]; j--)
        dp[0][j]=dp[0][j-weight[0]]+value[0];

    // weight数组的大小 就是物品个数
    for(int i = 1; i <= weight.size(); i++){
    // 遍历物品
		int num=weight[i-1])for (int j=1; j <= bagWeight; j++){
    // 遍历背包容量
            if(j < num])           //背包容量已经不足以拿第i个物品了
                dp[i][j] = dp[i-1][j]; //最大价值就是拿第i-1个物品的最大价值
            //背包容量足够拿第i个物品,可拿可不拿:
            //拿了最大价值是前i-1个物品扣除第i个物品的 重量的最大价值加上i个物品的价值
            //不拿就是前i-1个物品的最大价值,两者进行比较取较大的
            else
                dp[i][j]=max(dp[i-1][j], dp[ i-1][j-num]+value[i-1]);
        }
    }
    cout<<dp[weight.size()-1][bagWeight]<<endl;
}

滚动dp推导

二维代码可以进行优化,去除选取物品的那一层,简化为一维背包

在使用二维数组的时候,
递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把 dp[i - 1]那一层拷贝到dp[i] 上,
表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i-1]这一层拷贝到dp[i]上,不如只用一个一维数组dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

遍历背包的顺序是不一样的:
二维dp遍历的时候,背包容量是从小到大;一维dp遍历的时候,背包是从大到小

二维:
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。

一维:
如果正序遍历,那么物品0就会被重复加入多次,倒序遍历是为了保证物品i只被放入一次。
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

举例推导dp数组
输入:nums: [1, 1, 1, 1, 1], target = 4

一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:在这里插入图片描述

滚动dp模板


void bags(){
   
    vector<int> weight = {
   1, 3, 4};
    vector<int> value = {
   15, 20, 30};
    int bagWeight = 4;

    // 初始化
    // 一维 dp    状态定义:dp[j]表示容量为j的背包能放下东西的最大价值
    vector<int> dp(bagWeight + 1, 0);
    for (int i = 1; i < weight.size(); i++){
    // 遍历物品
    	int num=weight[i-1];
        for (int j = bagWeight; j >=num; j--){
   // 遍历背包容量(一定要逆序)
            dp[j] = max(dp[j], dp[j - num] + value[i-1]); //不取或者取第i个(从0开始)
        }
    }
    cout << dp[bagWeight] << endl;
}

解题思路

1、两层循环
分别遍历物品nums和背包容量target

2、转移方程
根据背包的分类:确定物品和容量遍历的先后顺序
根据问题的分类:确定状态转移方程的写法

背包分类的模板(一维dp)

(注意:这里背包是从下标为0开始遍历)
1、0/1背包:
外循环nums,
内循环target,
target倒序且target>=nums[i];

(01背包用的是上一轮的结果,所以要倒序)
//dp[j] 全部初始化为0
dp[j] = max(dp[j],dp[j- weight[i]] + value[i])
此时的dp[j]其实是跟 上一个i-1的时候的dp[j] 去比较 (上一个的dp[j]是不能取到第i件物品的)。
如果能取到第i个物品(第i件物品能不能放下),那么就比较不取第i件物品(dp[j])跟取到第i件物品的价值(dp[j- weight[i]] + value[i])。更新之后的dp[j]取较大值

dp[j]是能取第i件以及之前所有物品的最大价值,每次的比较都是围绕能取第i件(含之前的物品)以及第i-1件(含之前的物品)的价值。

2、完全背包:
外循环nums,
内循环target,
target正序且target>=nums[i];

完全背包中相反,采用顺序。前i件物品的dp[j-weight]+value,前i-1件物品的dp[j-weight],前i-1件物品的dp[j]这三者的比较。也就是当前状态,需要先推导出重量j之前的当前状态,才能得到最大值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值