【知识点7】背包问题⭐⭐⭐⭐⭐

背包问题不仅仅只适用于求最优解的情况,它同样适用于计数型dp,求解可行等问题~这些问题没有最优解的帽子,看起来就不那么容易想到背包了

今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o

0. 引言

背包问题是一类经典的动态规划问题,它有很多的形式,本节只介绍三种最简单的背包问题:

  • 0-1背包问题
  • 完全背包问题
  • 多重背包问题

而在这三种背包中,又以 0-1背包为重

之所以说“0-1背包最重要”是因为完全背包问题和多重背包问题都可以转换成0-1背包问题,而且大多数的背包问题是在0-1背包的基础上解决的

1. 多阶段动态规划问题

  • 定义
    有一类动态规划可解的问题,它可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,一般把这类问题称为 多阶段动态规划问题
  • 举例
    如下图所示,问题被分为了5个阶段,其中状态F属于阶段3,它由阶段2的状态C和状态D推得。显然,对这种问题,只需要从第一个问题开始,按照阶段顺序解决每个阶段中的状态的计算,就可以得到最后一个阶段中的状态的解。
    这对设计状态的具体含义是很有帮助的,01背包问题就是这样一个例子。01背包的阶段就是物品们的编号1~n,阶段中的状态就是状态数组dp[i][j]
  • ⭐在我看来,多阶段动态规划问题的dp数组的维度通常是2维的,一个维度是阶段,一个维度是阶段中的状态
  • 为什么要讲解这样一个多阶段呢?
    因为正是因为多阶段,当前阶段的状态只与前一阶段的状态有关,所以我们可以将背包问题中的二维数组转换成一维数组。
    在这里插入图片描述

2. 0-1背包问题

2.1 问题描述

在这里插入图片描述
0-1背包的注意事项

  • 背包可以不被装满(后面会有专门的题目说必须要求装满的);
  • 名字的由来:因为在最优解中,每个物品都有两种可能的情况,即在背包中或者不存在(背包中有0个该物品或者1个),所以我们把这个问题称为0-1背包问题

2.2 暴力解决方案

时间复杂度为: O ( 2 n ) O(2^n) O(2n)
在这里插入图片描述

2.3 动态规划解决方案

时间复杂度为: O ( n V ) O(nV) O(nV)

状态定义

dp[i][v]表示从前i件物品(1≤i≤n,0≤v≤V)选择物品装入容量为v的背包中所能获得的最大价值 怎么求解dp[i][v]呢?

考虑对第i件物品的选择策略,只有两种策略:

  1. 不放第i件物品,那么问题转换为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v]
  2. 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v-w[i]]+c[i]

状态转移方程

由于只有这两种策略,且要求获得最大价值,因此有:
在这里插入图片描述
上面这个就是状态转移方程。注意到dp[i][v]只与之前的状态dp[i-1][]有关,所以可以枚举i从1到n,v从0到V,通过边界dp[0][v] = 0(0≤v≤V)(即前0件物品放入任何容量v的背包中都只能获得价值0)就可以把整个dp数组递推出来。

因此可以写出代码:

for(int i=1;i<=n;i++){
	for(int v=w[i];v<=V;v++){
		dp[i][v] = max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
	}
} 

可以知道,时间复杂度和空间复杂度都是 O ( n V ) O(nV) O(nV),其中时间复杂度已经无法再优化,但是空间复杂度还可以再优化。

在这里插入图片描述
代码如下:

for(int i=1;i<=n;i++){
	for(v = V; v >= w[i] ;v--){		//逆序枚举v 
		dp[v] = max(dp[v],dp[v-w[i]]+c[i]);
	}
}

这样01背包问题就可以用一维数组表示来解决了,空间复杂度为 O ( V ) O(V) O(V)
特别说明:如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓;如果使用一维数组存放,则v的枚举必须是逆序!
完整的求解01背包问题的代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 100;	//物品最大件数
const int maxv = 1000;	//V的上限
int w[maxn],c[maxn],dp[maxv];

int main(){
	int n,V;
	scanf("%d%d",&n,&V);
	for(int i=1;i<=n;i++){
		scanf("%d",&w[i]);
	}
	for(int i=1;i<=n;i++){
		scanf("%d",&c[i]);
	}
	//边界
	for(int v=0;v<=V;v++){
		dp[v] = 0;
	}
	for(int i = 1;i<=n;i++){
		for(int v=V;v>=w[i];v--){
			//状态转移方程
			dp[v] = max(dp[v],dp[v-w[i]]+c[i]); 
		}
	}
	
	printf("%d\n",dp[V]);
	return 0; 
} 

事实上,对多阶段动态规划问题来说,都可以尝试把阶段作为状态的一维

如果当前设计的状态不满足无后效性,那么不妨把状态进行升维,即增加一维或若干维来表示相应的信息,这样可能就满足无后效性

2.4 恰好装满背包⭐⭐⭐⭐⭐

在这里插入图片描述
总结一下:

  • 无论怎样dp[0][0] = 0;
  • 如果是求最大值,那么dp[0][1~maxc]=-inf;(若大则负)
  • 如果是求最小值,那么dp[[0]1~maxc]=inf;(若小则正)

3. 完全背包问题

3.1 问题描述

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

3.2 状态定义

同样,dp[i][v]表示前i件物品放入容量为v的背包中能获得的最大价值

和01背包一样,完全背包问题的每种物品都有两种策略,但是也有不同点。对第i件物品来说:

  1. 不放第i件物品,那么dp[i][v] = dp[i-1][v],这步跟01背包是一样的;
  2. 放第i件物品。这里的处理和01背包有所不同。
    在这里插入图片描述

3.3 状态转移方程

在这里插入图片描述
看上去和01背包很像是不是?其实唯一的区别就在于max的第二个参数是dp[i]而不是dp[i-1]。而这个状态转移方程同样可以改成一维的形式,即:
在这里插入图片描述
写成一维形式之后和01背包完全相同,唯一区别在于这里v的枚举顺序是 正向枚举,而01背包的一维形式中v必须是 逆向枚举。完全背包的一维形式代码如下:
在这里插入图片描述

  • 怎么理解必须正向枚举呢?
    如下图所示,求解dp[i][v]需要它左边的dp[i][v-w[i]]和它上方的dp[i-1][v],显然如果让v从小到大枚举,dp[i][v-w[i]]就总是已经计算出的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,直接可以覆盖。
    在这里插入图片描述

4. 多重背包问题——二进制表示法⭐⭐⭐⭐⭐

在这里插入图片描述在这里插入图片描述
上面的二进制拆解就是用二进制的思想拆解数字k,从而可以 表示0~k中的任何数字 ,它是一种优化复杂度的方法。

其实也并不需要怎么特别理解,只需要举个例子就会了:

对于13,有:

13=[1+2+4] +6

对于7,有:

7=[1+2+4]

对于5,有:

5=[1+2]  +2

这样理解就好了~~

5. 题型训练

  1. 【0-1背包基础入门题】采药
  2. 【0-1背包基础入门题】点菜问题
  3. 【0-1背包之必须充满背包】最小邮票数
  4. 【完全背包基础入门,计数型背包】LeetCode 518. Coin Change 2
  5. 【完全背包之必须充满背包】Piggy-Bank
  6. 【多重背包】珍惜现在,感恩生活
  7. ⭐⭐⭐【多重背包之可行解型】POJ 1014 Dividing

6. 参考文档

  1. 算法笔记
  2. 王道机试指南
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值