01背包问题初探

本文探讨了01背包问题,介绍了如何将其转化为完全背包问题并应用动态规划求解。通过示例展示了如何构建状态转移矩阵,寻找最大价值,并通过反推确定物品的选择方案。动态规划方法具有O(log(∑i=1nti))的时间复杂度和O(log2(∑i=1nti))的空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目:
背包容量为12,有物品1,2可供选择

物品占用空间价值数量
物品1235
物品2342

求在背包中装入价值最大的解决方案。

2019/2/27
补充:背包问题分为9类,上面的问题严格来说属于完全背包问题,有特殊解法,参考https://www.cnblogs.com/jbelial/articles/2116074.html。
使用完全背包问题的解法后面附上。


思路:

  • 5个物品1可等价为5个物品,2个物品2可以等价为2个物品,这样一共有7个物品,转换为7个物品的0-1背包问题。
  • 对于第i个物品,背包问题的基本递推公式如下:
    1. Wi>W,装不下了,则M(i, W)=M(i-1, W);
    2. Wi<=W,那么选择装或者不装,则M(i, W)=max{M(i-1, W) , M(i-1, W-Wi)+Vi}。
  • 装第i个物品的价值由第i-1个物品有关,因此可以想到递归;同时,也可以选择将前i-1个物品的状态保存,使用动态规划。

结果:
max_val:17
num1:3, num2:2

由于本题目只有两类物品,枚举当然也是合适的,但是如果物品种类数t>=3,那么时间复杂度肯定是无法接受的。

枚举
def func1(x1,x2,space):
	max_val,num1,num2=0,[],[]
	for i in range(x1[2]+1):
		for j in range(x2[2]+1):
			if i*x1[0]+j*x2[0]<=space:
				if i*x1[1]+j*x2[1]>max_val:
					num1,num2=[i],[j]
					max_val=i*x1[1]+j*x2[1]
				elif i*x1[1]+j*x2[1]==max_val:
					num1.append(i)
					num2.append(j)
					max_val=i*x1[1]+j*x2[1]
	print('max_val:%d'%max_val)
	for i in range(len(num1)):
		print('num1:%d, num2:%d'%(num1[i],num2[i]))
x1=[2,3,5]
x2=[3,4,2]
space=12
func1(x1,x2,space)

注:物品种类数为n,数量分别为t1,t2,…,tn,使用枚举需要的时间复杂度O (∏i=1nti\prod_{i=1}^{n} t_ii=1nti),空间复杂度O (1).

递归:按照树深度搜索从上到下遍历
由于题目的物品数量比较多,不便画图,这里采取一个物品数量更少的01背包问题来解释思路。
背包容量为5,有物品1,2,3,4可供选择

物品占用空间价值数量
物品1231
物品2121
物品3341
物品4221

用w(w,v)状态变量表示选择第i个物品时的剩余空间和当前总价值。如下图,选择左子树表示选择放入当前物品,选择右子树表示不放入当前物品。
树图
由图可以看出,选择物品1,2,4或者物品1,3可同时达到最大价值7;因此要求最大价值,实际上就是返回叶子节点中的最大值以及对应的搜索路径。

def func2(info, w):
	max_val,state=bag_01(info,w)
	num=set()
	for clue in state:
		count_a,count_b=0,0
		for i in range(len(clue)):
			if clue[i]>0:
				if i<5:
					count_a+=1  
				else:
					count_b=count_b+1
		num.add((count_a,count_b))

	print('max_val:%d'%max_val)
	for elem in num:
		print('num1:%d, num2:%d'%(elem[0],elem[1]))


def bag_01(info,w):
	if info==[] or w[0]<info[0][0]:
		return w[1],[[]]
	x0,state0=bag_01(info[1:],w)
	x1,state1=bag_01(info[1:],[w[0]-info[0][0],w[1]+info[0][1]])
	if x0>x1:
		for clue in state0:
			clue.insert(0,0)
		return x0,state0
	elif x0<x1:
		for clue in state1:
			clue.insert(0,1)
		return x1,state1
	else:
		for clue in state0:
			clue.insert(0,0)
		for clue in state1:
			clue.insert(0,1)
		state0.extend(state1)
		return x0, state0
info=[[2,3] for i in range(5)]+[[3,4] for i in range(2)]
w=[12,0]
func2(info, w)

注:此方法中func中的两层遍历是为了将每个“单独的物品”还原到原来的两类物品。此方法时间复杂度 O (log(∑i=1nti\sum_{i=1}^{n} t_ii=1nti)),空间复杂度O (log2log^2log2(∑i=1nti\sum_{i=1}^{n} t_ii=1nti)).

动态规划:
动态规划求解背包问题变成了一个填表问题,仍然以递归算法中的背包问题为例:

给与空间012345
物品0000000
物品1003333
物品2023555
物品3023567
物品4023567

其中物品0和给与空间0只是为了递推的时候更方便,简化边界条件判断。

下面对表格的填写进行分析

  • 表格的每一行表示分别给与第i个物品0,1,2,3,4,5的空间,选择放入第i个物品与否,达到的最大价值W(i,j);
  • 第i个物品在空间给与空间为j时,如果w(i)>j,则W(i,j)=W(i-1,j);否则,W(i,j)=max{ v(i)+W(i-1, j-w(i)), W(i-1,j) };
  • 例如:对于物品1,在给与空间为0,1时,放不下,则最大价值为W(i-1,j)=0,在给与空间为2时,W(1,2)=max{ v(1)+W(0,0), W(0,2)}=max{3,0}=3,给与空间为3,4,5时W=3;对于物品2,给与空间为0,1,2,3,4,5时对应的最大价值分别为0,2,3,5,5,5。

于是表格填写完也就找到了最大价值

但是如何找到具体怎么选择物品呢?
表格中存在答案。

表格是很有规律的,

  • 最大价值必定在表格最右下角;
  • 对于每个物品选择与否,取决于对应的状态变量w(w,v)是否存在。
这个例子中有两种方案都达到最大价值7,分别是选择1,2,4和1,3.

表格
分析:用(x,y)i表示对于物品i的(给与空间,最大价值),对于物品4,(5,7)4,如果选择物品4,那么对于物品3,(3,5)3,如果不选择物品4,则对于物品3,(5,7)3。可以发现在物品3中,第3列最大价值为5,第5列最大价值为7,说明这两种都是合理的。于是从表的最后一行逐层往上反推便可以得到分配方案,递归便可以解决这个问题。

def func3(info,space):	
	info=[[0,0]]+info
	w=[[0 for i in range(space+1)] for j in range(len(info))]
	find_maxval(w, info)
	max_val=w[-1][-1]
	state=find_state(w, info, len(w)-1, len(w[0])-1)
	num=set()
	for clue in state:
		count_a,count_b=0,0
		for i in range(len(clue)):
			if clue[i]>0:
				if i<5:
					count_a+=1  
				else:
					count_b=count_b+1
		num.add((count_a,count_b))
	print('max_val:%d'%max_val)
	for elem in num:
		print('num1:%d, num2:%d'%(elem[0],elem[1]))

def find_maxval(w, info):
	for i in range(1,len(w)):
		for j in range(1,len(w[0])):
			if info[i][0]>j:
				w[i][j]=w[i-1][j]
			else:
				w[i][j]=max(w[i-1][j], w[i-1][j-info[i][0]]+info[i][1])
def find_state(w,info,row,col):
	if row==0:
		return [[]]
	
	if w[row-1][col]>w[row-1][col-info[row][0]]+info[row][1] or \
		info[row][0]>col:
		clue=find_state(w,info,row-1,col)
		for elem in clue:
			elem.append(0)
		return clue
	elif w[row-1][col]<w[row-1][col-info[row][0]]+info[row][1]:
		clue=find_state(w,info,row-1,col-info[row][0])
		for elem in clue:
			elem.append(1)
		return clue	
	else:
		clue0=find_state(w,info,row-1,col)
		clue1=find_state(w,info,row-1,col-info[row][0])
		for elem in clue0:
			elem.append(0)
		for elem in clue1:
			elem.append(1)
		return clue0+clue1
	
info=[[2,3] for i in range(5)]+[[3,4] for i in range(2)]
space=12
func3(info,space)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值