【dp】背包问题

news2024/11/19 15:12:47

背包问题

  • 一、背包问题概述
  • 二、01背包问题
    • (1)求这个背包至多能装多大价值的物品?
    • (2)若背包恰好装满,求至多能装多大价值的物品?
  • 三、完全背包问题
    • (1)求这个背包至多能装多大价值的物品?
    • (2)若背包恰好装满,求至多能装多大价值的物品?

一、背包问题概述

背包问题是⼀种组合优化的问题。问题可以描述为:给定⼀组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。

根据物品的个数,分为如下几类:

  • 01背包问题:每个物品只有⼀个
  • 完全背包问题:每个物品有无限多个
  • 多重背包问题:每件物品最多有 x 个
  • 混合背包问题:每个物品会有上面三种情况
  • 分组背包问题:物品有 n 组,每组物品里有若干个,每组里最多选⼀个物品

其中上述分类里面,根据背包是否装满,又分为两类:

  • 不一定装满背包
  • 背包一定装满

根据限定条件的个数,又分为两类:

  • 限定条件只有⼀个:比如体积 -> 普通的背包问题
  • 限定条件有两个:比如体积 + 重量 -> 二维费用背包问题

虽然背包问题种类非常繁多,题型非常丰富,难度也是非常难以捉摸。但是,它们都是从 01背包问题 演化过来的。01 背包问题 非常重要。

二、01背包问题

01背包 — 模板

Nowcoder -DP41.01背包
题目:你有一个背包,最多能容纳的体积是V。
现在有 n 个物品,第 i 个物品的体积为 vi,价值为 wi.
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数 n 和 V,表示物品个数和背包体积。
接下来 n 行,每行两个数 vi 和 wi,表示第i个物品的体积和价值。
1 ≤ n, V, vi, wi ≤ 1000
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。

(1)求这个背包至多能装多大价值的物品?

  • 状态表示
    dp[i][j] 表示:从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最大价值。
  • 状态转移方程
    线性 dp 状态转移方程分析方式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:
    a. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时 dp[i][j] = dp[i - 1][j]
    b. 选择第 i 个物品:那么我就只能去前 i - 1 个物品中,挑选总体积不超过 j - v[i] 的物品。此时 dp[i][j] = dp[i - 1][j - v[i]] + w[i] 。但是这种状态不⼀定存在,因此需要特判⼀下。

具体来说,如下图:

在这里插入图片描述

  • 初始化
    我们多加一行,方便我们的初始化,此时仅需将第⼀行初始化为 0 即可。因为什么也不选,也能满足体积不小于 j 的情况,此时的价值为 0 。

综上,状态转移方程为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])

第一问的核心代码如下:

		// 第一问
	    // dp[i][j] 表示:从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最⼤价值
	    for (int i = 1; i <= n; i++)
	    {
	        for (int j = 1; j <= V; j++)
	        {
	            dp[i][j] = dp[i - 1][j];
	            if (j - v[i] >= 0)
	                dp[i][j] = max(w[i] + dp[i - 1][j - v[i]], dp[i][j]);
	        }
	    }
	    cout << dp[n][V] << endl;

(2)若背包恰好装满,求至多能装多大价值的物品?

第⼆问仅需微调⼀下 dp 过程的细节即可,因为有可能凑不齐 j 体积的物品,因此我们把不合法的状态设置为 -1.

  • 状态表示
    dp[i][j] 表示:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出来的最大价值。
  • 状态转移方程
    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) . 但是在使用 dp[i - 1][j - v[i]] 的时候,不仅要判断 j >= v[i] ,还要判断 dp[i -1][j - v[i]] 表示的情况是否存在,也就是 dp[i - 1][j - v[i]] != -1.

我们可以表示为下图的:

在这里插入图片描述

  • 初始化
    我们多加一行,方便我们的初始化:
    i. 第⼀个格子为 0 ,因为正好能凑齐体积为 0 的背包;
    ii. 但是第一行后面的格子都是 -1 ,因为没有物品,无法满足体积大于 0 的情况,如下图所示,dp 表完成初始化:

在这里插入图片描述

所以第二问的核心代码如下:

		// 第二问
	    // dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出来的最⼤价值。
	    memset(dp, 0, sizeof(dp));
	
	    // 值为 -1 表示从 0~i 的物品中没有体积刚好为 j 的物品,所以也就没有价值
	    for (int j = 1; j <= V; j++) dp[0][j] = -1;
	
	    for (int i = 1; i <= n; i++)
	    {
	        for (int j = 1; j <= V; j++)
	        {
	            dp[i][j] = dp[i - 1][j];
	            if (j - v[i] >= 0 && dp[i - 1][j - v[i]] != -1)
	                dp[i][j] = max(dp[i][j], w[i] + dp[i - 1][j - v[i]]);
	        }
	    }
	    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;

空间优化:
背包问题基本上都是利用 「滚动数组」 来做空间上的优化:
i. 利用「滚动数组」优化;
ii. 直接在「原始代码」上修改。

根据状态转移方程,我们更新当前 dp 表位置的时候,只需要用到 i - 1 行中的第 j 个位置和第 j - v[i] 个位置,如下图,三角形是我们需要更新的位置,我们只需要两个圆圈的位置:

在这里插入图片描述

我们可以观察到,三角形所在的位置只需要依赖第 j 个位置和第 j - v[i] 个位置,所以我们可以大胆把横坐标去掉,只需要一个维度的坐标即可,这种方法叫做滚动数组;但是我们要注意,遍历顺序需要从右往左,如下图:

在这里插入图片描述

因为我们依赖的是当前未更新的 dp 表的位置和当前位置左边的位置,如果从左往右更新,那么对于后面的位置来说,它们的左边位置已经被覆盖了,所以我们应该从右往左更新。

所以在01背包问题中,优化的结果为:
i. 删掉所有的横坐标;
ii. 修改⼀下 j 的遍历顺序

优化后的整体代码:

	#include <vector>
	#include <algorithm>
	#include <string.h>
	#include <iostream>
	using namespace std;
	
	const int N = 1001;
	int n, V, v[N], w[N];
	int dp[N];
	
	// 对空间进行优化:使用滚动数组
	int main()
	{
	    cin >> n >> V;
	    for (int i = 1; i <= n; i++)
	        cin >> v[i] >> w[i];
	
	    // 第一问
	    // dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最⼤价值
	    for (int i = 1; i <= n; i++)
	    {
	        for (int j = V; j >= v[i]; j--)  // 遍历顺序修改成从右往左
	            dp[j] = max(w[i] + dp[j - v[i]], dp[j]);
	    }
	    cout << dp[V] << endl;
	
	
	    // 第二问
	    // dp[i][j] 表⽰:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出来的最⼤价值。
	    memset(dp, 0, sizeof(dp));
	
	    // 值为 -1 表示从 0~i 的物品中没有体积刚好为 j 的物品,所以也就没有价值
	    for (int j = 1; j <= V; j++) dp[j] = -1;
	
	    for (int i = 1; i <= n; i++)
	    {
	        for (int j = V; j >= v[i]; j--)
	            if (dp[j - v[i]] != -1)
	                dp[j] = max(dp[j], w[i] + dp[j - v[i]]);
	    }
	    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
	
	    return 0;
	}

有关01背包的练习题:
Leetcode -416.分割等和子集
Leetcode -494.目标和
Leetcode -1049.最后一块石头的重量Ⅱ

三、完全背包问题

完全背包 — 模板

Nowcoder -DP42.完全背包
题目:你有一个背包,最多能容纳的体积是V。
现在有 n 种物品,每种物品有任意多个,第 i 种物品的体积为 vi, 价值为 wi.
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数 n 和 V,表示物品个数和背包体积。
接下来 n 行,每行两个数 vi 和 wi,表示第i种物品的体积和价值。
1 ≤ n, V ≤ 1000
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。

(1)求这个背包至多能装多大价值的物品?

  • 状态表示:
    dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j ,所有的选法中,能挑选出来的最大价值。(这里是和 01背包⼀样)
  • 状态转移方程:线性 dp 状态转移⽅程分析方式,⼀般都是根据最后⼀步的状况,来分情况讨论。但是最后⼀个物品能选很多个,因此我们的需要分很多情况:
    a. 选 0 个第 i 个物品:此时相当于就是去前 i - 1 个物品中挑选,总体积不超过 j 。此时最大价值为 dp[i - 1][j]
    b. 选 1 个第 i 个物品:此时相当于就是去前 i - 1 个物品中挑选,总体积不超过 j - v[i] 。因为挑选了⼀个 i 物品,此时最大价值为 dp[i - 1][j - v[i]] + w[i]
    c. 选 2 个第 i 个物品:此时相当于就是去前 i - 1 个物品中挑选,总体积不超过 j - 2 * v[i] 。因为挑选了两个 i 物品,此时最大价值为 dp[i - 1][j - 2 * v[i]] + 2 * w[i]
    d. …

如下图:

在这里插入图片描述

此时我们可以如下分析:

在这里插入图片描述

我们观察到,画绿色下划线的内容中,下面的下划线中的 dp 表达式与上面的只相差一个 w[i] ,所以,紫色框框中的 dp[i][j-v[i]] 加上一个 w[i] 是可以完全替代上面的紫色框框中的一堆表达式,所以我们得出以下状态转移方程:

dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]]+w[i])

  • 初始化:
    我们多加⼀行,方便我们的初始化,此时仅需将第⼀行初始化为 0 即可。因为什么也不选,也能满足体积不小于 j 的情况,此时的价值为 0 。

所以第一问的核心代码如下:

		// 第一问
	    for(int i = 1; i <= n; i++)
	    {
	        for(int j = 0; j <= V; j++)
	        {
	            dp[i][j] = dp[i - 1][j];
	            if(j >= v[i]) dp[i][j] = max(dp[i][j - v[i]] + w[i], dp[i][j]);
	        }
	    }
	    cout << dp[n][V] << endl;

(2)若背包恰好装满,求至多能装多大价值的物品?

第⼆问仅需微调⼀下 dp 过程的细节即可,因为有可能凑不齐 j 体积的物品,因此我们把不合法的状态设置为 -1 。

  • 状态表示
    dp[i][j] 表示:从前 i 个物品中挑选,总体积正好等于 j ,所有的选法中,能挑选出来的最大价值。

  • 状态转移方程
    dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]) ;但是在使用 dp[i][j - v[i]] 的时候,不仅要判断 j >= v[i] ,还要判断 dp[i][j - v[i]] 表示的情况是否存在,也就是 dp[i][j - v[i]] != -1.

  • 初始化
    我们多加一行,方便我们的初始化:
    a. 第⼀个格子为 0 ,因为正好能凑齐体积为 0 的背包;
    b. 但是第一行后面的格子都是 -1 ,因为没有物品,无法满足体积大于 0 的情况。

所以第二问的核心代码如下:

	    // 第二问
	    memset(dp, 0, sizeof(dp));
	    dp[0][0] = 0;
	    for(int j = 1; j <= V; j++)
	        dp[0][j] = -1;
	
	    for(int i = 1; i <= n; i++)
	    {
	        for(int j = 0; j <= V; j++)
	        {
	            dp[i][j] = dp[i - 1][j];
	            if(j >= v[i] && dp[i][j - v[i]] != -1) dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
	        }
	    }
	    cout << (dp[n][V] == -1? 0 : dp[n][V]) << endl;

空间优化: 滚动数组,注意,根据状态转移方程,我们这里需要更新的位置是依赖 i - 1 行的第 j 个位置和第 i 行的 j - v[i] 个位置,而 dp[i][j-v[i]] 是已经更新过的位置,所以我们需要从右往左更新 dp 表;

在这里插入图片描述

空间优化后的整体代码:

		#include <iostream>
		#include <string.h>
		using namespace std;
		
		const int N = 1001;
		int n, V, v[N], w[N];
		int dp[N];
		
		// 空间优化后的代码
		int main() 
		{
		    cin >> n >> V;
		    for(int i = 1; i <= n; i++)
		        cin >> v[i] >> w[i];
		
		    // 第一问
		    for(int i = 1; i <= n; i++)
		    {
		        for(int j = 0; j <= V; j++)
		        {
		            if(j >= v[i]) dp[j] = max(dp[j - v[i]] + w[i], dp[j]);
		        }
		    }
		    cout << dp[V] << endl;
		
		    // 第二问
		    memset(dp, 0, sizeof(dp));
		    dp[0] = 0;
		    for(int j = 1; j <= V; j++)
		        dp[j] = -1;
		
		    for(int i = 1; i <= n; i++)
		    {
		        for(int j = 0; j <= V; j++)
		        {
		            if(j >= v[i] && dp[j - v[i]] != -1) dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
		        }
		    }
		    cout << (dp[V] == -1? 0 : dp[V]) << endl;
		}

完全背包的练习题:
Leetcode -322.零钱兑换
Leetcode -518.零钱兑换Ⅱ
Leetcode -279.完全平方数

此外,我们还有一些⼆维费用的背包问题练习:
Leetcode -474.一零和
Leetcode -879.盈利计划

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

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

相关文章

有点奇葩的消息——CEO 2 个月之前也离职了

这个消息还是有点奇葩的。 在找工作一段时间后&#xff0c;有原公司的同事联系了下互相通报了下各自的情况。 如有看过&#xff1a;北美 2023 被裁员的感悟 这篇文章的大致都知道公司在4个月前&#xff0c;也就是2023年6月份的时候进行了大规模的裁员&#xff0c;公司也只保留…

多源蒸馏域适应

方法 D是域判别器&#xff0c;C是分类器。阶段3选择更接近目标的源训练样本用来微调C。阶段4对于每个源域&#xff0c;基于阶段2学到的目标编码器提取图像特征。接着结合每个源分类器的不同预测获得最终预测Result( x T x_T xT​) ∑ i 1 N w i C i ′ ( F i T ( x T ) ) \sum…

C++-封装unordered

本期我们来封装实现unordered系列&#xff0c;需要前置知识&#xff0c;没有看过哈希的建议先看看哈希&#xff0c;而且哈希的代码都在这里面&#xff0c;一会要用到 C-哈希Hash-CSDN博客 目录 代码实现 迭代器 const迭代器 全部代码 代码实现 首先我们要把V改为T&#xff…

【回顾一下Docker的基本用法】

文章目录 回顾一下Docker的基本用法1.初识Docker1.1.什么是Docker1.1.1.应用部署的环境问题1.1.2.Docker解决依赖兼容问题1.1.3.Docker解决操作系统环境差异1.1.4.小结 1.2.Docker和虚拟机的区别1.3.Docker架构1.3.1.镜像和容器1.3.2.DockerHub1.3.3.Docker架构1.3.4.小结 1.4.…

计算摄像技术02 - 颜色空间

一些计算摄像技术知识内容的整理&#xff1a;颜色视觉与感知特性、颜色空间和基于彩色滤镜阵列的彩色感知。 文章目录 一、颜色视觉与感知特性 &#xff08;1&#xff09;色调 &#xff08;2&#xff09;饱和度 &#xff08;3&#xff09;明度 二、颜色空间 &#xff08;1&…

2023版 STM32实战7 通用同步/异步收发器(串口)F103/F407

串口简介和习惯 -1-通用同步异步收发器 (USART) 能够灵活地与外部设备进行全双工数据交换&#xff0c;满足外部设备对工业标准 NRZ 异步串行数据格式的要求。 -2-硬件流控制一般是关闭的 -3-波特率指单位时间传输bit个数 -4-数据位一般是8位 -5-一般无校验位 编写代码思路 -1-参…

位图/布隆过滤器

一、位图 1.1位图的概念 所谓位图&#xff0c;就是用每一位来存放某种状态&#xff0c;适用于海量数据&#xff0c;数据无重复的场景。通常是用来判断某个数据存不存在的。 1.2位图的实现 template<size_t N>class bitset{public:bitset(){//需要N个比特位&#xff0c;…

SpringBoot客户端实现分片、断点上传

客户端的分片上传和断点续传是指将一个文件拆分为多个分片&#xff0c;在网络不稳定或上传中断后&#xff0c;可以从中断处继续上传而不需要重新上传整个文件。 当需要上传大型文件时&#xff0c;客户端的分片上传和断点续传是一种常用的技术。它可以提高上传效率并减少网络中断…

【计算机网络面试题(62道)】

文章目录 计算机网络面试题&#xff08;62道&#xff09;基础1.**说下计算机网络体系结构2.说一下每一层对应的网络协议有哪些&#xff1f;3.那么数据在各层之间是怎么传输的呢&#xff1f; 网络综合4.**从浏览器地址栏输入 url 到显示主页的过程&#xff1f;5.说说 DNS 的解析…

Vscode配置C#编程环境(win10)

目录 1、安装好Vscode 2、下载安装.NetCore SDK 3、配置C#环境 3.1 打开Vscode并下载扩展 3.2 Vscode中打开文件夹并配置环境 3.3 调试运行 1、安装好Vscode 2、下载安装.NetCore SDK 官网如下&#xff0c;下载完成后双击打开一路走到底就行.NetCore SDK官网 软件显示安…

利用freesurfer6进行海马分割的环境配置和步骤,以及获取海马体积

利用freesurfer6进行海马分割的环境配置和步骤 Matlab Runtime 安装1. 运行recon-all&#xff1a;2. 利用 recon-all -s subj -hippocampal-subfields-T1 进行海马分割3. 结束后需要在/$SUBJECTS_DIR/subject/的文件夹/mri路径下输入下面的代码查看分割情况4. 在文件SUBJECTS_D…

连续波雷达相关内容简介

连续波雷达(CW雷达)连续发射高频信号。回波信号被永久接收和处理。 一、未调制连续波雷达 未调制的连续波雷达除了振荡的相位之外没有其他时间参考。它只能用于测量小于波长的距离。以上的一切都极为含糊。因此,它只能用作多普勒雷达或控制已知距离的恒定性。 二、调频连…

VUE3照本宣科——eslint与prettier

VUE3照本宣科——eslint与prettier VUE3照本宣科系列导航 前言一、eslint1.配置文件2.配置规则3.忽略错误 二、prettier三、总结 VUE3照本宣科系列导航 1.VUE3照本宣科——认识VUE3 2.VUE3照本宣科——应用实例API与setup 3.VUE3照本宣科——响应式与生命周期钩子 4.VUE3照本宣…

关于Go语言的底层,Channel

1.Channel 介绍一下Channel&#xff08;有缓冲和无缓冲&#xff09; Go 语言中&#xff0c;不要通过共享内存来通信&#xff0c;而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型&#xff0c;中文可以叫做通信顺序进程&#xff0c;是通过 gor…

剑指offer——JZ54 二叉树搜索树的第k个节点 解题思路与具体代码【C++】

一、题目描述与要求 二叉搜索树的第k个节点_牛客题霸_牛客网 (nowcoder.com) 题目描述 给定一棵结点数为n 二叉搜索树&#xff0c;请找出其中的第 k 小的TreeNode结点值。 1.返回第k小的节点值即可 2.不能查找的情况&#xff0c;如二叉树为空&#xff0c;则返回-1&#xf…

Sql server 使用DBCC Shrinkfile 收缩日志文件

磁盘空间有限&#xff0c;需要收缩日志文件释放空间。 数据库名称上右击属性->文件,逻辑名称日志文件默认名称为“_log”结尾。 alter database 数据库 set recovery simple dbcc shrinkfile(XXX_log,2,truncateonly) alter database 数据库 set recovery full

webp批量转换为png、jpg工具

webp批量转换为png、jpg工具 链接&#xff1a;https://pan.baidu.com/s/1NZArgbiF88_qBbAIiUR4qQ 提取码&#xff1a;2sqs –来自百度网盘超级会员V5的分享

IO流 之 转换流( InputStreamReader 字节输入转换流 和 OutputStreamWriter 字节输出转换流)

当文本文件和代码的编码不一致时&#xff0c;使用字符流会导致读取出来的文字乱码。如下&#xff1a; 读取文件的编码时GBK编码。 代码的编码时UTF-8的编码。 程序运行出来中文则是乱码。 这里就要使用到转换流了。 字节转换流 InputStreamReader 字节输入转换流 使用步骤…

一文搞懂时间序列ARIMA模型

文章目录 1 ARIMA的定义2 差分(differencing)2.1 Order&#xff1a;差分的阶数2.2 Lag&#xff1a;差分的滞后2.3 滞后运算/滞后算子/延迟算子2.4 关于差分的两个误解 3 ARIMA的平稳性4 ACF与PACF5 时序模型的选择与评估5.1 超参数p、q、d的确定5.2 时间序列的评估指标 1 ARIMA…

Linux自用笔记

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Linux相关 ✨特色专栏&#xff1a; My…