背包问题详解

01背包问题以及优化

其实在写题解的时候不是在告诉别人,而是在教会自己!

作者的话

  • 本篇题解面向想学算法的小白,或者对背包问题百思不得其解的小伙伴
  • 作者尽量采用了不同的视角,不同的方式去描述和解决问题
    • 题解模板
    • 图解算法
    • 手工模拟
  • 希望大家喜欢!
  • 由于作者水平有限,如果有路过的小伙伴或者大神看出本篇题解的问题或者不足,还请评论!
  • 本人感激不尽!

01背包问题的赘述

用一句话描述背包问题

背包问题就是在有限集里面的最优解问题

  • 有限的物品(每个物品仅有一个)
  • 有限的空间(限制条件)
  • 求最大值

01背包问题的代码(未优化)

#include <bits/stdc++.h>
using namespace std;
int n, m;				//n代表物品个数,m代表背包空间
const int N = 1010;
int f[N][N];			//f[i][j]里面存放的数据为,空间为 j 时,有 i 个物品可选择时的,最大值
int v[N], w[N];

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> v[i] >> w[i];
	}
	f[0][0] = 0;		//状态初始化,但是因为f[][]是全局变量,所有自动初始化,所有本行可加可不加
	//精华内容见文章分析
	//start
	for (int i = 1; i <= n; ++i) {
		for (int j = 0; j <= m; ++j) {
			f[i][j] = f[i - 1][j];
			if (j >= v[i]) {
				f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
			}
		}
	}
	//end
	cout << f[n][m];
	return 0;
}

01背包问题的精华代码解析

抛开代码和动态规划,如果让我们来做选择,我们会怎么做?

  1. 先挑最贵的拿
  2. 然后再去尽可能的去塞满空间

但是,我们说上面的那种拿法很可能不是最优解

贪心并不能保证最优解,只有理智的思考才能到达彼岸

所有我们选择借助表格来帮助判断

首先,规定一下横向和纵向分别代表什么意思

image-20220317162735201

然后,你一步步的填完整张表格

image-20220317163329241

最后,你果断选择了拿音响和吉他,却放弃了笔记本

其实,你可知道,你已经做了一次动态规划

动态规划就是把所有可能的组合不重不漏的列出来,然后循环取其中的最大值。

铺垫了这么多,不知道你是否理解这 短短五行代码的含义了

for (int i = 1; i <= n; ++i) {
		for (int j = 0; j <= m; ++j) {
			f[i][j] = f[i-1][j ];
			if (j >= v[i]) {
				f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
			}
		}
	}
  1. 为什么是从1开始?

    代码是有实际意义的!

    i 代表物品,如果没有物品,那么价值肯定是0,所有从 0 开始循环没有任何意义。

  2. 内循环,j 表示背包空间。

  3. 如果能保证 j > v[i] 也就是把内循环的判断条件改一下,那么就可以直接写

    f[i][j] = max(f[i][j-1], f[i - 1][j - v[i]] + w[i]);
    
  4. 第二次判断条件,可以放到 for 循环里面去判断

  5. 状态转移方程

    • 01是什么意思,0 代表不取整个物品, 1 代表取这个物品
    • 动态规划是递推,尾递归或者递归是递归。(递推的下一个值是简历在前面值已知的基础上,而递归则是从顶部开始,一步步去逼近答案)
    • 我们按照最后一个物品取还是不取(这里的最后一个物品并不是全部物品里面的最后一个物品,而是每次外循环所代表的物品总数的最后一个物品)

优化代码

#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 1010;
int f[N];
int v[N], w[N];

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> v[i] >> w[i];
	}
    //精华部分
    //start
	for (int i = 1; i <= n; ++i) {
		for (int j = m; j >= v[i]; --j) {
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}
    //end
	cout << f[m];
	return 0;
}

优化的代码的精华部分解析

可能你还没看出啦优化了哪里

原本存放结果的二维数组被压缩成一维的了

即,由 f[N][N] ==> f[N]

可以注意到,与未优化的代码相比,背包的空间是由 从小到大循环 变成了 从大到小循环

下面就来解释一下从大到小循环的妙处

其核心问题就是

代码里面两个 f [ j ] 是否相同

image-20220317214040320

正确答案是:不相同

下面,请听我,细细道来。

  • 先说运算符的优先级问题, “=” 的优先级仅比 “,” 高。所有,先算右边的比较,再进行赋值。
  • 那么,第二个 f [ j ] 代表的是什么? 很明显代表上一轮的 j 容量下最大的价值, 也就是 f [ i-1 ][ j ]
  • 所有第一个是更新后的 j 内存下的最大值

为什么要逆序循环

结论 :因为正序循环,第二个f [ i ] 代表这一轮的 也就是 **f [ i ] [ j ] **

给想真正学懂的小伙伴一个建议,那就是自己去亲自用纸演算一遍

实在不想动手的可以看我下面的演算过程

假设:

物品数量为 3, 背包空间为 4;

物品空间价值
1.吉他11500
2.音响43000
3.笔记本电脑32000

逆序

 f(4) = max(f(4), f(3)+1500)      //此时 f(3) = 0 && f(4) = 0 ==> f(4) = 1500
 f(3) = max(f(3), f(2)+1500)      //下面的原因类似,不再赘述
 f(2) = max(f(2), f(1)+1500) 
 f(1) = max(f(1), f(0)+1500)
 ————————————————————————————————————————————————————————————————————————————————————————————————————
 f(4) = max(f(4), f(0)+3000)	  //此时, f(0) = 0 && f(4) = 1500 ==> f(4) = 3000
 f(3) = f(3)    				 //因为 j < v[i], 所有并没有更新下面这些值
 f(2) = f(2) 
 f(1) = f(1)
 ---------------------------------------------------------------------------------------------------
f(4) = max(f(4), f(1)+2000)		 //此时, f(1) = 1500 && f(4) = 3000 ==> f(4) = 3500
f(3) = max(f(3), f(0)+2000)      //此时, f(0) = 0 && f(3) = 1500 ==> f(4) = 2000
f(2) = f(2) 				    因为 j < v[i], 所有并没有更新下面这些值
f(1) = f(1)

好了,逆序我写完了,正序改你自己尝试一下了吧

想了一下,还是写上吧。

正序

f(1) = max(f(1), f(0) + 1500)		//f(0) = 0 && f(1) = 0 ==> f(1) = 1500
f(2) = max(f(2), f(1) + 1500)		//f(1) = 1500 && f(2) = 0 ==> f(2) = 3000
f(3) = max(f(3), f(2) + 1500)		//f(2) = 3000 && f(3) = 0 ==> f(3) = 4500
f(4) = max(f(4), f(3) + 1500)		//f(3) = 4500 && f(4) = 0 ==> f(4) = 6000
—————————————————————————————————————————————————————————————————————————————————————————————————————
f(1) = f(1)						   //以下三行, 因为 j < v[i] ,所有数据并没有更新
f(2) = f(2)
f(3) = f(3)
f(4) = max(f(4), f(1) + 3000)		//f(1) = 1500 && f(4) = 6000 ==> f(4) = 6000
-----------------------------------------------------------------------------------------------------
f(1) = f(1)						   //以下两行, 因为 j < v[i] ,所有数据并没有更新
f(2) = f(2)
f(3) = max(f(3), f(0)+2000)			//f(3) = 4500 && f(0) = 0 ==> f(3) = 4500
f(4) = max(f(4), f(1)+2000)			//f(4) = 6000 && f(1) = 1500 ==> f(4) = 6000

后记

不知道聪明的你发现了没有,其实正序就是完全背包问题的解法

赘述一下完全背包问题是什么问题

  1. 背包空间有限
  2. 每种物品的数量无限

可能你还会发现另一个问题,一个关于完全背包的问题的本质

计算每种物品, 单位空间的价值

在本次假设中:

​ 吉他的单位空间的价值是1500

​ 音响的单位空间的价值是750

​ 笔记本电脑的单位价值是2000/3

好了好了,扯远了!再不结束,可能作者整个人就不好了

预告一下:

接下来还会出 高精 或者 八大排序的算法题解,但由于作者精力有限,不能在短时间内肝出这么多的题解。

如果有哪个算法题解一直困扰着你,还请在评论区打出来。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值