ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

一文教会01背包

2022-01-22 21:02:34  阅读:100  来源: 互联网

标签:遍历 15 一文 01 数组 物品 背包 dp


01背包

01背包指的是一种题目模板

如果说有很多东西 每个东西只能拿一次 要装到一个背包里 背包有体积上限

这就是经典的01背包问题

在我们下意识背出模板的时候

我们先想一下 01背包问题的暴力解法是什么

在01背包中 对于每件物品 要么取 要么不取 所以一共有2^n种情况

这是回溯+爆搜的做法

为了优化时间复杂度 我们引入了动态规划(DP)

为了解释动态规划

我决定引入一个DP五部曲的概念(——摘自《代码随想录》)

  • 确定dp数组(dp table)以及下标的含义
  • 确定递推公式
  • dp数组如何初始化
  • 确定遍历顺序
  • 举例推导dp数组

下面是01背包的模板题

N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

i件物品的体积是 vi,价值是 wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 vi, wi, 用空格隔开,分别表示第 i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V≤1000

0 < v i , w i ≤ 1000 0<vi,wi≤1000 0<vi,wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

朴素的做法是这样的

 	int n, V;
    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 = 1; j <= V; j++)
    	{ 
    		if(j >= v[i])	dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
    		else	dp[i][j] = dp[i-1][j];
    	}
    	
    cout << dp[n][V] << endl;

对于背包问题其实状态都是可以压缩的

在使用二维数组的时候,递推公式:
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]);

压缩后的一维做法

 	int n, V;
    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 = V; j >= w[i]; j--)
            dp[j] = max(dp[j], dp[j - w[i]] + value[i]);
    cout << dp[V] << endl;

我们对01背包进行DP五部曲分析

1.确定dp数组的定义

在一维版的dp数组中,dp[j]代表当体积为j时,所背物品的最大价值

2.递推公式

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

3.对于数组的初始化

关于初始化 我们要考虑到第1点和第2点,在实际问题中 我们一般会把3和4联系在一起想

因为dp[j]表示:容量为j的背包,可以装下的最大价值是dp[j]

那么当j = 0 的时候 什么也装不了 dp[0] = 0;

再来看到递推公式

因为dp数组的意义是最大价值 那么当价值不为负的时候,非0下标初始化为0就可以了

但是当价值为负的时候 ,非0下标必须初始化到负无穷,不然初始值就会覆盖掉最大价值

//插 cpp如何把数组初始化成负无穷
memset(d, 0x8f, sizeof(d));

4.数组遍历的顺序

不同于一些线性DP的问题,背包问题在递推公式并没有直接给出递推的方向

for(int i = 1; i <= n; i++)
	for(int j = V; j >= w[i]; j--)
		dp[j] = max(dp[j], dp[j-w[i]] + value[i]);

这里我们突然发现 滚动数组的遍历写法和二维数组的遍历写法有些不同

为什么内嵌循环的循环顺序是从大到小呢?

答:倒序遍历是因为保证每个物品只被放入背包一次

这里可能有些不太好理解

我们举一个例子:物品0重量w[0] = 1, value[0] = 15;

正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30

物品0被加入了两次,怎么会是呢?

倒序遍历:

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15

从后往前遍历的话,每次取得的状态不会和之前取得的状态发生重叠,这样保证了每件物品加入一次

那么问题又来了,为什么二维数组中不需要倒序遍历呢?

因为二维数组中的dp[i] [j] 都是通过dp[i-1] [j]来得到的,本层的并不会被覆盖

此处较为拗口,建议实践出真知

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

达咩!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

如果你不能理解的话,建议将for循环颠倒一下输出dp数组观察一下

5.举例推导

​ 重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:

第一次遍历物品0 0 | 15 | 15 | 15 | 15

第二次遍历物品1 0 | 15 | 15 | 20 | 35

第三次遍历物品2 0 | 15 | 15 | 20 | 35

标签:遍历,15,一文,01,数组,物品,背包,dp
来源: https://blog.csdn.net/DM010maker/article/details/122643509

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有