动态规划算法(4)01背包问题

news2025/2/23 19:12:46

文章目录

  • 01背包
    • 完整代码
  • 滚动数组优化:01背包
    • 完整代码

上节回顾:
动态规划(3)最大方案数问题

01背包

问题引入:

有n个物品,每个物品的重量分别是 weight[i],每个物品的价值分别是 value[i]。你有一个背包,这个背包共有w 容量,请问你要怎么分配物品,才能使得背包中的物品总价值最高呢?

重量价值
物品0115
物品1320
物品2430
你的背包的容量: 6

这道题是典型的01背包问题,当然你也可以使用暴力来解决这个问题。
即使用回溯法,依次把每一个物品放入背包中,然后依次计算它的最大值,不过这样的方法的时间复杂度将会非常高,所以我们使用动态规划的思想来解决这个问题,而动态规划的具体实现方法则是01背包问题

解决动态规划问题的五个步骤:

  1. 确定dp数组以及其下标的含义。
  2. 确定递推公式
  3. dp数组的初始化
  4. dp数组的遍历顺序
  5. 图例推导dp过程

首先我们以这道题为例:

  1. 确定dp数组以及其下标的含义

我们创建dp[i][j]二维数组,i 表示dp二维数组的行,表示物品的编号(从0开始);j 表示dp二维数组的列,表示背包容量为 j 时,所能装下的最大价值。

i : 物品编号,表示物品 i
j : 背包的当前容量,当前容量为 j
dp[i][j] :把 编号为 i 的物品放入 容量为 j 的背包,其所能容纳的物品的最大价值是dp[i][j]。


  1. 确定递推公式

把物品放入背包,我们有两种方案:

  • 不放入背包
  • 放入背包

什么时候不能把物品放入背包呢?
如果我们之前已经把某一个物品放入了背包,假设之前的物品的容量(我们定为编号i -1)是3,而我们的背包容量是4,而当前的物品(编号 i )的容量是2,显然我们无法把当前的编号为 i 的物品放入背包,因为我们的背包总容量不够,所以,我们无法放入这个物品到背包中,此时我们的递推公式为: dp[i][j]=dp[i-1][j]
简单来说:当前容量为 j 的背包无法容量 编号i的物品,所以此时的dp[i][j] 仍然等于上一次的背包存储的物品的价值dp[i-1][j]。

什么时候要把物品放入背包呢?
首先我们的背包容量 j 必须大于即将放的这个物品的重量,即 j >weight[i],我们可以放入物品 i 到这个背包。dp[i-1][j-weight[i]] 为我们的背包容量为 i-weight[i] 时,不放物品 i 时的最大价值(等同于不放入背包的递推公式)。在这个时候,我们又放入了物品i,因此此时放入了物品i 的背包的最大价值:dp[i-1][j-weight[i]]+value[i]

综上我们的递推公式:


dp[i][j]=max(dp[i-1][j] , dp[i-1][j-weight[i]]+value[i])


提示:关于为什么要加max ,原因是:

  • 你放入了编号 i 物品,那么物品放入后使得当前的背包价值最大呢?
  • 还是不放入这个物品,而之前放入的物品(i-1)保持不动,使得背包的价值最大呢?

两者取一个最大值即可算出当前背包容量为 j 时的最佳方案,即局部最优解

  1. dp数组如何初始化

由于dp数组中使用了 i -1 等需要以前的计算结果的过程,所以我们必须对dp数组进行初始化。
由于dp数组是一个二维数组,所以我们有必要对第一行与第一列进行初始化,以防万一。

  • 第一列的初始化 dp[i][0]:物品的编号为 i,背包的当前容量是0,所以无论是哪个物品,一定放不进背包,所以: dp[i][0]=0
  • 第一行的初始化 dp[0][j]:物品的编号为 0(第一个物品),背包的当前容量 j (j>=1),所以一定能放入物品编号为0的物品,因此dp[0][j]=value[0]
//第一列的初始化
for (int i=0;i<物品的总数;i++)
{
	dp[i][0]=0;
}
//第一行的初始化
for (int j=weight[0];j<=w;j++)
{	
	dp[0][j]=value[0];
}

在这里插入图片描述


  1. dp数组的遍历顺序

根据我们的递推公式:dp[i][j]=max(dp[i-1][j] , dp[i-1][j-weight[i]]+value[i])
因此我们需要首先得到dp[i-1][j] 和dp[i-1][j-weight[i]]的值,而这两个值在二维数组中位于我们的(i,j)的左上方(正上方),所以我们必须从上方和左边开始往下边和右边进行遍历,依次填充。

在这里插入图片描述

01背包具有两种遍历和填充的方式:

  • 首先遍历物品,然后遍历背包容量
  • 首先遍历背包容量,然后遍历物品

这里给出两种方式:
num:物品的数量

  1. 首先遍历物品,然后遍历背包容量:
for (int i=1;i<num;i++)
{
	for (int j=0;j<w;j++)
	{
		if (j < weight[i]) dp[i][j] = dp[i - 1][j]; 
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
	}
}

首先遍历每一行,然后再填充行中的每一列,注意经过之前的初始化,我们的 i 只需要从1开始就行了,i =0 就是第一行的初始化,已经放入我们的背包中了。

  1. 首先遍历背包容量,然后遍历物品
for (int j=0;j<=w;j++)
{
	for (int i=1;i<num;i++)
	{
		if (j < weight[i]) dp[i][j] = dp[i - 1][j]; 
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
	}
}

首先遍历每一列,然后再填充列中的每一行,同理 i 从1开始即可,这两种方案都是一致的。

因为递推公式是一致的。


  1. 图例推导dp数组

在这里插入图片描述


完整代码

//二维dp数组
void bag_question()
{
	//物品的重量与价值
	vector<int> weight{ 1,3,4 };
	vector<int> value{ 15,20,30 };
	int num = weight.size();
	//背包的容量(大于等于物品的最大容量,否则就无法放下这个物品)
	int bagweight = 4;
	//创建二维dp背包数组:dp[i][j] i:编号i的物品 0-1-2   j:背包的容量j     0-1-2-3-4-5-6
	//这是一个 3x7 的二维数组
	vector<vector<int>> dp(num, vector<int>(4 + 1));

	int m = dp.size();
	int n = dp[0].size();
	//dp数组的初始化
	//1. 第一列: j=0,背包的容量为0,无法放任何物品
	for (int i = 0; i < m; i++)
	{
		dp[i][0] = 0;
	}
	//1. 第一行: i=0,背包的容量>=1,一定可以放下第一个物品(编号0),保存其价值
	for (int j = weight[0]; j <= bagweight; j++)
	{
		dp[0][j] = value[0];
	}
#if 0
	//先遍历物品,再遍历背包,跳过编号0的物品
	for (int i = 1; i < num; i++)
	{
		for (int j = 0; j <= bagweight; j++)
		{
			//背包无法容纳这个物品: dp[i][j]=dp[i-1][j]
			if (j<weight[i])
			{
				dp[i][j] = dp[i - 1][j];
			}
			//背包可以容纳这个物品:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[0]]+value);
			else
			{
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			}
		}
	}
#else
	//先遍历背包,再遍历物品,跳过容量0的背包容量
	for (int j = 0; j <= bagweight; j++)
	{
		for (int i = 1; i < num; i++)
		{
			if (j < weight[i])
			{
				dp[i][j] = dp[i - 1][j];
			}
			else
			{
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			}
		}
	}
#endif
	for (auto& x : dp)
	{
		for (auto& y : x)
		{
			cout << y << " ";
		}
		cout << endl;
	}
	cout << dp[num - 1].back() << endl;
}

测试: 在这里插入图片描述


滚动数组优化:01背包

滚动数组:就是把二维dp数组降为一维dp数组

使用滚动数组的条件:需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

  1. 确定dp数组及其下标关系

因此,我们完全可以把dp[i][j]:把第i个物品放入背包容量为j时的价值。
优化成dp[j]:背包容量为 j 时的价值。

遇到某个物品仍然有两种选择:

  • 要么不放这个物品,还等于其本身: dp[j]=dp[j]
  • 要么放这个物品:dp[j]=d[j-weight[i]]+value[i]
  1. 递推公式的推导

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


  1. dp数组的初始化

初始化: dp[0]=0

  1. dp数组的遍历

一维dp与二维dp的不同之处:
二维dp的写法中,遍历背包的顺序是不一样的!

一维dp的遍历:
num是物品的总数,w是背包的总容量

for (int i=0;i<num;i++)
{
	for (int j=w;j>=weight[i];j--)
	{
		dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
	}
}

可以发现我们遍历背包重量的时候是逆序的!

为什么呢? 举一个例子: 如果是正序;
物品重量:10 ,价值 20
背包总容量: 20

dp[ 1 ]=dp[ 1-weight[0] ]+value[0] = 20
dp[ 2 ]=dp[2-weight[0] ]+value[0]=20 + 20 =40

进行了两次计算dp[1]的值,因此我们需要采用逆序:

dp[ 2 ]=dp[2-weight[0] ]+value[0]= 20
dp[ 1 ]=dp[ 1-weight[0] ]+value[0] = 20


  1. 图例推导dp过程:

在这里插入图片描述


完整代码

//一维dp数组
void bag_question_2()
{
	//物品的重量与价值
	vector<int> weight{ 1,3,4 };
	vector<int> value{ 15,20,30 };
	int num = weight.size();
	//背包的容量(大于等于物品的最大容量,否则就无法放下这个物品)
	int bagweight = 4;
	//创建一维dp背包数组
	vector<int> dp(bagweight + 1);
	int m = dp.size();
	//dp数组的初始化
	//1. dp[0]表示背包容量为0,无法放任何物品,因此初始化为0
	dp[0] = 0;
	//先遍历物品,再遍历背包,跳过编号0的物品
	for (int i = 0; i < num; i++)
	{
		for (int j = bagweight; j >= weight[i]; j--)
		{
			dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
		}
	}
	for (auto& x : dp)
	{
		cout << x << " ";
	}
	cout << endl;
	cout << dp.back() << endl;
}

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

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

相关文章

11场面试无一被拒!Alibaba Java面试参考指南真香

今年基本算是结束了&#xff0c;很多小伙伴都开始准备明年的金三银四了。准备面试肯定是要想办法提升自己的面试能力&#xff0c;这个时候如果还去一昧地提升自己的代码能力对面试是毫无帮助的。大多数人在面试的时候都会遇到以下几种情况&#xff08;大家可以看看自己中了几个…

Redis深度历险

开篇&#xff1a;授人以鱼不若授人以渔—— Redis 可以用来做什么&#xff1f; 小册的内容范围 并没有涵盖 Redis 全部的内容知识点&#xff0c;比如 Redis 内置的 lua 脚本引擎就完全没有提 到Redis 基础数据结构Redis 有 5 种基础数据结构&#xff0c;分别为&#xff1a;stri…

[2022-12-06]神经网络与深度学习hw11 - 各种优化算法比较

contentshw11 - 优化算法比较写在开头task1题目内容题目思路题目解答题目总结task2题目内容题目思路题目解答题目总结task3题目内容题目思路题目解答题目总结task4题目内容题目思路题目解答题目总结task5题目内容题目解答题目总结task6题目内容题目解答task7题目内容题目解答hw…

Git代码提交规范

Git代码提交规范 1.安装commitizen和cz-customizable npm install -g commitizen4.2.4 npm i cz-customizable6.3.0 --save-dev2.在package.json中进行新增 "config": {"commitizen": {"path": "node_modules/cz-customizable"} }{…

Linux系统移植五:启动开发板并测试

往期文章 Linux系统移植一&#xff1a;移植U-BOOT 添加自己的板子并编译&#xff08;非petalinux版&#xff09; Linux系统移植二&#xff1a;生成fsbl引导文件并制作BOOT.bin Linux系统移植三&#xff1a;移植Kernel生成zImage和dtb文件 Linux系统移植四&#xff1a;Petalinu…

链接概念介绍

链接器 为了更好地理解计算机程序的编译和链接的过程&#xff0c;我们简单地回顾计算机程序开发的历史一定会非常有益。计算机的程序开发并非从一开始就有着这么复杂的自动化编译、链接过程。原始的链接概念远在高级程序语言发明之前就已经存在了&#xff0c;在最开始的时候&a…

Ubuntu - 搭建samba服务器

安装samba程序 使用如下命令安装samba sudo apt-get install samba sudo apt-get install smbclient验证是否安装成功&#xff0c;查看samba版本 samba -V配置samba服务器 samba的配置文件所在位置为&#xff1a;/etc/samba/smb.conf&#xff0c;使用vim命令修改配置 sudo…

[激光原理与应用-40]:《光电检测技术-7》- 常见光干涉仪及其应用

目录 第1章 干涉仪概述 1.1 什么是干涉仪 1.2 基本原理 1.3 分类 1.4 应用 1.5 干涉仪的类型 第2章 常见光干涉仪 2.1 迈克尔逊干涉仪 2.2 泰曼-格林干涉仪 2.3 移相干涉测量仪 2.4 菲索共路干涉仪 第1章 干涉仪概述 1.1 什么是干涉仪 干涉仪是很广泛的一类实验技…

Vue3中 子组件v-model绑定props接收到的父组件值报update:modelValue错

开发过程中二次封装了一个搜索的组件&#xff0c;子组件内使用了el-select和el-input 参数分别对应父组件传入的selectValue和selectText参数 子组件内部change和input事件来同步触发组件中数据的修改 最终本地开发环境一切正常&#xff0c;部署到测试环境和生产环境后出现下…

java测试示例-生成ULID

ULID全称Universally Unique Lexicographically Sortable Identifier&#xff0c;直译就是通用唯一按字典排序的标识符&#xff0c;原始仓库是https://github.com/ulid/javascript&#xff0c;由前端开发者alizain发起&#xff0c;基于JavaScript语言。从项目中的commit历史来看…

基于java(ssm)留学生交流互动论坛系统源码(java毕业设计)

基于java&#xff08;ssm&#xff09;留学生交流互动论坛系统 留学生交流互动论坛系统&#xff0c;是基于java编程语言&#xff0c;mysql数据库&#xff0c;ssm框架和idea工具开发&#xff0c;本系统主要分为留学生&#xff0c;管理员两个角色&#xff0c;其中留学生可以注册登…

Vue中的过滤器(管道)

过滤器&#xff1a;将指定的数据&#xff0c;按照一套流程过滤加工&#xff0c;最后返回一个过滤之后的值 注册局部过滤器 将过滤器写在filters配置项中的是局部过滤器&#xff0c;只供该vue匹配的容器使用 new Vue({el: #root,data: function(){return {time: 1670297916166}}…

JVM之内存区域划分、类加载和垃圾回收

文章目录前言一、JVM内存区域划分二、类加载1.类加载是什么&#xff1f;2.类加载的过程3.何时触发类加载&#xff1f;4.双亲委派模型三、垃圾回收&#xff08;GC&#xff09;1.GC是什么&#xff1f;2.GC回收哪部分内容&#xff1f;3.怎么回收&#xff1f;&#xff08;1&#xf…

Rust 跑简单的例子

Rust 一门赋予每个人构建可靠且高效软件能力的语言 安装 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh 提示失败 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh info: downloading installer curl: (60) SSL certificate problem: certifi…

FastDFS搭建及整合Nginx实现文件上传

一、准备环境 FastDFS需要两个服务&#xff0c;一个tracker跟踪器&#xff0c;一个storage存储节点&#xff0c;tracker做调度配置&#xff0c;storage完成文件存储上传等功能。 这里我们使用两台虚拟机服务器&#xff08;centos 7)来部署&#xff0c;有条件的同学建议直接上云…

Vue中多条件图片路径通过Map存储获取避免嵌套if-else

场景 若依前后端分离版手把手教你本地搭建环境并运行项目&#xff1a; 若依前后端分离版手把手教你本地搭建环境并运行项目_霸道流氓气质的博客-CSDN博客_前后端分离项目本地运行 前端接收到后台数据之后需进行多个条件判断进而显示对应的图片路径。 比如先判断车辆的类型、…

第十三章:AQS

AQS 基础概念为什么 AQS 是 JUC 最重要的基石&#xff1f;AQS 能干什么AQS内部结构AQS内部类NodeAQS 源码分析以 lock方法为入口讲解nonfairTryAcquire 方法addWaiter方法线程B线程CacquireQueued 方法B节点C节点unlockcancelAcquire 方法总结AQS 基础概念 AQS 全称&#xff1…

【树莓派】了解wiringPi库、控制继电器

目录一、wiringPi库二、继电器1、继电器介绍及接线说明2、树莓派控制继电器一、wiringPi库 wiringPi是一个很棒的树莓派IO控制库&#xff0c;使用C语言开发&#xff0c;提供了丰富的接口&#xff1a;GPIO控制&#xff0c;中断&#xff0c;多线程等。 在树莓派命令行输入gpio -…

供应商管理软件有哪些特点和优势?

在这个快节奏的商业环境中&#xff0c;企业常常需要同时处理多个供应商。手动处理所有这些流程会有不少困难&#xff0c;为了克服这个问题&#xff0c;供应商管理软件是市场上可用的最佳解决方案。 好用的供应商管理软件&#xff0c;比如广受客户好评的8Manage SRM&#xff0c…

Spring 长事务导致connection closed,又熬了一个大夜!

大家好&#xff0c;我是不才陈某~ 是的&#xff0c;今早一到公司就收到了机器人的告警&#xff0c;从异常日志来看是数据库连接已关闭&#xff0c;然后我在解决这个问题的过程中发现了几个问题&#xff0c;不急&#xff0c;听我一一道来 异常被try后没有继续抛出&#xff0c;导…