DAY45:动态规划(五)背包问题:01背包理论基础+二维DP解决01背包问题

news2024/12/23 7:30:27

文章目录

    • 背包问题大纲
    • 01背包
    • 01背包暴力解法
    • 01背包二维DP解法
      • 二维DP数组的解法
        • DP数组含义
        • 递推公式
        • 初始化二维DP数组(比较重要)
        • 遍历顺序(比较重要)
      • 二维DP数组完整版
        • 思路总结
        • 返回值为什么是二维数组最后一个元素
        • DP推导过程与数组含义进一步理解
        • 递推公式理解

对于面试的话,掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包

背包问题大纲

在这里插入图片描述
leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。

而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。

所以背包问题的理论基础重中之重是01背包

leetcode上没有纯背包题目,都是用背包问题的思想去解决和应用,也就是需要转化为01背包问题

01背包

01背包是有N种物品,每种物品只有一个。完全背包是有N种物品,每种物品有无限个

多重背包是有N种物品,每种物品个数各不相同。

这几类问题主要体现在物品个数不同

纯粹的01背包问题如下所示:

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

在这里插入图片描述
在这里插入图片描述

01背包暴力解法

每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 O(2^n),这里的n表示物品数量。

暴力的解法是指数级别的时间复杂度。所以才需要动态规划的解法来进行优化

#include<bits/stdc++.h>
using namespace std;

struct Item {
    int weight;
    int value;
};

int maxValue = 0;
Item items[3] = {{1, 15}, {3, 20}, {4, 30}};
int n = 3;
int capacity = 4;

void backtrack(int i, int totalWeight, int totalValue) {
    if (i == n || totalWeight == capacity) {
        if (totalValue > maxValue) maxValue = totalValue;
        return;
    }
    backtrack(i + 1, totalWeight, totalValue);
    if (totalWeight + items[i].weight <= capacity)
        backtrack(i + 1, totalWeight + items[i].weight, totalValue + items[i].value);
}

int main() {
    backtrack(0, 0, 0);
    cout << "最大价值是:" << maxValue << endl;
    return 0;
}

backtrack 函数尝试包括或不包括当前位置的物品,如果包括,那么将它的价值和重量加到总价值和总重量中。在每次选择后,程序递归到下一个物品。如果到达物品列表的末尾,或者已经达到背包的最大容量,该函数会检查是否找到了更高的价值,并更新全局最大值(maxValue)。

01背包二维DP解法

二维DP数组的解法

我们先用二维数组的方式去求解。

DP数组含义

我们先定义一个二维DP数组dp。dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

递推公式

确定递推公式的时候,我们要确定dp[i][j]可以由哪几个方向推出来。当前背包的状态就取决于是不是放入物品i。放入物品i是一个状态,不放物品i又是另一个状态。

  • 如果不放物品i背包容量为j,那么背包当前的最大价值为:dp[i-1][j],也就是背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i,那么放物品i的最大价值,就是背包容量-物品i的容量所能放的最大价值+物品i的价值dp[i-1][j-weight[i]]+value[i]

放物品i和不放物品i的两个值,取最大值,就是dp[i][j]对应的遍历到i情况下的最大价值

//递推公式
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);

初始化二维DP数组(比较重要)

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

首先由递推公式可以看出,dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]),因此为了避免数组下标越界,必须满足i>=1,i=0的情况需要单独分析;也就是dp[0][j]需要进行初始化

同时j-weight[i]因为是不固定数值,因此for循环里单独写if(j<=weight[i]) dp[i][j]=dp[i-1][j]即可。从这个角度出发,我们为了dp[i][j]=dp[i-1][j]这个式子成立,也需要对dp[0][j]进行初始化

关于dp[i][0]是否需要初始化的问题,实际上因为j会对if(j<weight[i])单独做判断,因此没有必要初始化dp[i][0],j从0开始是完全可以的。

DP数组情况如下图所示。

在这里插入图片描述

  • 初始化的时候注意dp数组的定义,dp数组定义是第i个东西放进背包时,背包的最大价值为dp[i],因此当物品i为0的时候,初始化第一行dp[0][j]就要看物品0放进去的时候,背包有多少价值
  • 同理,初始化第一列的时候,也是看背包容量为0的时候,对应价值是多少(全是0)
  • 实际上只初始化i=0就可以了,j=0不需要初始化因为遍历会单独写条件
vector<vector<int>>dp(n+1,vector<int>(n+1,0));//全部初始化为0
for(int j=0;j<=bagweight;j++){
    if(j<weight[0]) dp[0][j] = 0;//如果背包容量小于放入物品0的weight,dp[i][j]=0
    else
        dp[0][j]=value[0];
}

遍历顺序(比较重要)

背包类的题目要有两层for循环。一个for遍历物品i,另一个遍历背包容量j。

实际上,对于二维DP数组实现的01背包这两层for循环的内外嵌套是可以颠倒的。也就是说先遍历物品/背包都是可以的。

我们重新观察这个DP数组,可以发现,因为遍历到i的时候,要么dp取值是放入i,要么是不放入i。因此i的状态是由i上方的元素和左上方的元素决定的。(图中红色三角和绿色三角)

在这里插入图片描述
也就是说,我们遍历到i的时候,需要保证i的左上方和正上方都有数值。因此我们把两个先后顺序进行举例,如下图:(橙色三角是已经初始化的部分)

在这里插入图片描述
可以看出,无论是先遍历背包还是先遍历物品,都能保证当前遍历元素的左上方和正上方都有数值。因此两种遍历方式都可以。

二维DP数组完整版

背包最大重量为4。

物品重量和价值为:

在这里插入图片描述
问背包能背的物品最大价值是多少?

  • 递推公式思想同样是,对于每一个背包大小,都计算出当前背包大小能存放物品的最大价值,那么遍历到指定背包大小的时候,也是最大价值。DP递推公式对于每一个背包数值都适用。
  • 递推公式的大小比较,是已经装了上个物品的状态和能够装下当前物品,并且已经装了当前物品的状态比较。因此if(j<weight[i])这个判断只是在看当前的背包容量能不能给新来的物品腾地方,只要j>=weight[i]+1,就说明能给当前物品腾出位置。
#include <bits/stdc++.h>
using namespace std;

//传入的是背包容量与物品重量和价值
int knapsack(vector<int>& weight, vector<int>& value, int bagWeight) {
    int n = weight.size();//获取物品数量
    //创建i*j二维数组,i是物品,j是背包容量
    vector<vector<int>>dp(n+1,vector<int>(bagWeight+1,0));
    //二维数组初始化
    for(int j=0;j<=bagWeight;j++){
        if(j<weight[0]) continue; //保持0的数值
        else
            dp[0][j] = value[0];//第一行物品0的情况,也就是i=0那一行的情况
    }
    //遍历物品和背包
    for(int i=1;i<n;i++){
        for(int j=0;j<=bagWeight;j++){
            //看看当前的背包容量能不能给新来的物品腾地方,只要j>=weight[i]+1,就说明能给当前物品腾出位置,当前物品不是在和装了上个物品的状态比较,而是在和没有当前物品的状态比较
            if(j<weight[i]) dp[i][j]=dp[i-1][j];/
            else{
                //放这个i和不放这个i的情况对比,选最大的
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
            }
        }
    }
    return dp[n-1][bagWeight];
}

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

    cout << knapsack(weight, value, bagWeight) << endl;
}

int main() {
    test_knapsack();
}

思路总结

对于每一种物品(纵轴上的索引i),对于每一个背包大小(横轴上的索引j),都计算出当前背包大小能存放物品的最大价值。如果当前背包的容量无法装下物品i,那么dp[i][j]的值就等于dp[i-1][j],否则,需要在“不放入物品i”和“放入物品i”这两种选择中选取价值最大的,即max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])

这样,遍历到指定背包大小的时候,得到的就是当前背包可以装下物品的最大价值。所以说,这个DP递推公式对于每一个背包数值都适用

我们需要时刻注意,遍历到每一个物品的时候dp[i][j]表示的都是:在考虑前i个物品,并且背包容量为j的情况下,背包可以装下的最大价值

返回值为什么是二维数组最后一个元素

到了最后,就是考虑第[0–n]个物品,背包为j的时候,可以装下的最大价值。因此最后的结果,就是二维数组的最后一个元素。(考虑到了最后一个物品)。这也是DP数组含义相关的问题。

DP推导过程与数组含义进一步理解

当我们不清楚返回值or不清楚递推逻辑的时候,一定要先回去看DP数组与其关联下标的含义

本题中,DP数组的含义就是,考虑第[0–i]个物品,背包容量为j的情况下,dp[i][j]代表的就是当前背包的最大价值

物品个数为3,容量为4,对应value是表格中的情况,DP数组预期如下:

在这里插入图片描述
在动态规划的过程中,对于每一个背包的大小(j)和每一个物品(i),我们需要决定是否将物品i放入背包。这里的决策基于两种情况的比较:

  1. 不放入物品i:这种情况下,背包的价值等于没有考虑物品i时,只考虑前i-1个物品,背包容量为j时候,背包的最大价值,即dp[i-1][j]。(看DP数组的定义)
  2. 放入物品i:这种情况下,只有当背包的容量j大于等于物品i的重量weight[i]时,才能将物品i放入背包。放入物品i后,背包的价值等于考虑前i-1个物品,且背包剩余容量j-weight[i]时的最大价值加上物品i的价值,也就是dp[i-1][j-weight[i]]+value[i]。(也是通过DP数组含义推算得到)

递推公式理解

只要 j >= weight[i] ,就说明背包的容量足以容纳当前的物品。

if (j < weight[i]) 这个判断在检查当前背包的容量是否足以装下当前考虑的物品。如果背包的容量不足以装下当前的物品,那么 dp[i][j] 的值就应当等于没有装入当前物品,背包容量为j,且只装入前 i - 1 个物品时的最大价值,即 dp[i - 1][j]

如果 j >= weight[i],说明背包的容量足以装下当前的物品。此时,我们需要在两种选择之间取最优:一种是选择不装入当前的物品,背包的总价值即为 dp[i - 1][j];另一种是选择装入当前的物品,然后在剩余的背包容量中尽可能装入更多价值的物品,此时的背包总价值即为 dp[i - 1][j - weight[i]] + value[i]dp[i][j] 即为这两种选择中的最大值,这就是动态规划的状态转移方程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/746101.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

selenium 根据期刊信息获取知网文献信息 pt.1

哈喽大家好&#xff0c;我是咸鱼 之前写过一篇获取知网文献信息的文章&#xff0c;看了下后台数据还挺不错 所以咸鱼决定再写一篇知网文献信息爬取的文章 需要注意的是文章只是针对某一特定期刊的爬取&#xff0c;希望小伙伴们把关注点放在如何分析网页以及如何定位元素上面…

python实现前后端学生管理系统(前后端不分离)

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;项目。 目录 1、前言2、简述实现内容首页注册登录管理员 3、详细代码3.1、项目目录3.2、templates3.2.1、testxz…

赛效:如何在线转换图片格式

1&#xff1a;点击左侧菜单栏里的“格式转换”&#xff0c;然后在转换格式菜单栏里点击上传按钮。 2&#xff1a;选择下方输出格式&#xff0c;点击右下角“开始转换”。 3&#xff1a;稍等片刻转换成功后&#xff0c;点击图片右下角的“下载”&#xff0c;将转换后的图片保存到…

UE5《Electric Dreams》项目PCG技术解析 之 PCGCustomNodes详解(四)ApplyHierarchy

继续解析《Electric Dreams》项目中的自定义节点和子图&#xff1a;ApplyHierarchy 文章目录 前导文章标准组合拳ApplyHierarchyExecute with ContextIteration Loop BodyPoint Loop Body应用场景 小结 前导文章 《UE5《Electric Dreams》项目PCG技术解析 之 理解Assembly&…

【Android】APT与JavaPoet学习与实战

PS&#xff1a;本文讲解的APT全称为Annotation Processing Tool&#xff0c;而非是Android Performance Tuner&#xff0c;这两种工具简称皆为APT&#xff0c;前者是“注释处理工具”&#xff0c;后者是“Android性能调试器”。 本文分别使用Java、kotlin 语言进行开发&#xf…

做一个游戏小项目有多简单?

认识一个朋友&#xff0c;学了很多年的 python, 还停留在 helloworld 阶段&#xff0c;每次拿起又放下&#xff0c;是不是很熟悉&#xff1f;每天都在想&#xff0c;我要学编程&#xff0c;我要学编程&#xff0c;但是又不知道从何学起&#xff0c;学了一点又不知道怎么用&…

java并发编程原理-----线程

目录 上下文切换 java代码创建线程的两种方式 线程的五个状态 线程join方法 多线程之间的影响 上下文切换 CPU的每一个核心同一时刻只能执行一个线程&#xff0c;但是我们会发现电脑同一时刻现实会进行几千个线程&#xff0c;这就是cpu在快速的切换执行线程&#xff0c;由…

Python中的迭代器

一、介绍 在Python中&#xff0c;迭代器是一种访问集合元素的方式&#xff0c;可以用于遍历数据集中的元素&#xff0c;而不需要事先知道集合的大小。迭代器可以被用于循环语句中&#xff0c;例如for循环&#xff0c;来遍历集合中的每个元素。 Python中的迭代器是一个实现了迭…

将Windows系统上的音频、视频通过iTunes传输到iPhone上

这个地方需要下载安装版的iTunes 下载地址&#xff1a; https://www.apple.com/itunes/download/win64 不要从Windows的APP Store中下载iTunes&#xff0c;不好使。 安装完成后&#xff0c;如果是导入一个文件夹中的资料&#xff0c;则点击 【文件】》【将文件夹添加到资料库】…

岛屿数量 (力扣) dfs + bfs (JAVA)

给你一个由 ‘1’&#xff08;陆地&#xff09;和 ‘0’&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外&#xff0c;你可以假设该网格的…

在内卷化竞争时代,金龙鱼重返增长的关键到底是什么?

提到欧丽薇兰、胡姬花、香满园、海皇、金味、丰苑、锐龙洁劲100、丸庄酱油等品牌,多数消费者的第一反应是什么?多数消费者认为是某个不知名的新品牌。问题的重点不在产品&#xff0c;而在主品牌定位。 事实上&#xff0c;这都是金龙鱼母公司益海嘉里旗下的品牌。内行都知道益海…

“坏邻居”导致的kafka生产者TPS下降原因排查

背景&#xff1a; 今天测试了两种不同的场景下kafka producer的tps性能数据&#xff0c;两种场景下都是使用3个线程&#xff0c;每个线程都是对应一个kafka producer&#xff0c;测试发送到kafka集群的消息的量&#xff0c;两个场景的区别是场景A只发送kafka消息&#xff0c;场…

自定义类型

目录 什么是自定义类型 结构体 结构体的声明 常规结构体的声明形式 特殊的结构体声明形式 匿名结构体&#xff1a; 匿名结构体的重命名&#xff1a; 注意事项&#xff1a; 结构体的自引用 什么是结构体的自引用 结构体变量的定义与初始化 方法一&#xff1a; 方法…

总结python安装包(库)过程中的采坑

绝大数的包比如numpy、pandas可以用pip install或者conda install解决&#xff0c;使用pip时可以用pip -V命令看一下自己的pip安装在了哪个虚拟环境&#xff0c;一般pip安装在哪默认就把python包安装在哪。 pip -VC:\Users\20478>pip -V pip 23.1.2 from D:\Python\lib\sit…

Android Java代码与JNI交互 JNI子线程访问Java方法 (八)

🔥 Android Studio 版本 🔥 🔥 创建包含JNI的类 JNIInvokeMethod.java 🔥 package com.cmake.ndk1.jni;import com.cmake.ndk1.base.ICallbackMethod; import com.cmake.ndk1.base.IThreadCallback;public class JNIInvokeMethod {static {System.loadLibrary("…

VBA代码如何切换word和excel(3)

【分享成果&#xff0c;随喜正能量】人不能因为一件好事&#xff0c;高兴一整年&#xff0c;却能因为一个创伤&#xff0c;郁郁终生。痛苦给人的刺激&#xff0c;总是远远大于快乐。成年人的烦恼&#xff0c;和谁说都不合适&#xff0c;悲喜自渡&#xff0c;他人难悟。人最强大…

DDOS防御,阻止DDoS攻击的15个独家技巧

DDoS攻击可以使企业完全宕机数小时以上&#xff0c;而宕机的后果可能很严重&#xff0c;各种规模的企业和政府都可能受到影响。2021年&#xff0c;由于系统中断一小时导致销售额大幅下降&#xff0c;亚马逊为此遭受了约3400万美元的直接财务损失。而随后由于Fakebook的服务中断…

Spring源码系列-第2章-后置工厂处理器和Bean生命周期

第2章-后置工厂处理器和Bean生命周期 后置工厂处理器属于后置处理器&#xff0c;后置处理器是Spring最核心的部分&#xff0c;Spring几乎所有的附加功能全由它完成。 什么是BeanPostProcessor&#xff1f; public interface BeanPostProcessor {/*** Apply this {code BeanPos…

桥接(Bridge)模式

目录 动机使用场景参与者优劣协作实现相关模式应用和思考 桥接模式是将抽象部分和它的实现部分分离&#xff0c;使他们都可以独立的变化的对象结构型模式。桥接模式通过将继承改为组合的方式来解决问题&#xff1b;具体来说就是抽取其中一个维度并使之成为独立的类层次。 动机…

gma 2 教程(二)数据操作:2. 功能逻辑架构和栅格数据类型简介

功能逻辑架构 gma栅格数据操作所含功能/属性的关系结构如下图所示&#xff1a; 栅格数据类型 gma栅格数据类型继承自GDAL&#xff0c;与NumPy数据关联&#xff0c;但又有所不同&#xff0c;详细关系见下表&#xff1a; 栅格格式支持 栅格格式信息统计 gma继承了GDAL全部的栅格…