背包问题笔记

0-1背包:

01背包问题可描述为如下问题:

有一个容量为V的背包,还有n个物体。现在忽略物体实际几何形状,只要背包的剩余容量大于等于物体体积,那就可以装进背包里。每个物体都有两个属性,即体积w和价值v。
问:如何向背包装物体才能使背包中物体的总价值最大?

为什么不用贪心?

我在第一次做这个题目时考虑的是贪心算法。所谓贪心问题,就是每一步决策都采取最优解,按照此方案最后结果也是最优解。
为什么这个问题不能用贪心呢?
举个例子
我的背包容量为10,而且有4个物体,它们的体积和价值分别为
w1 = 8, v1 = 9
w2 = 3, v2 = 3
w3 = 4, v3 = 4
w4 = 3, v4 = 3
贪心是每一步采取最优拿法,即每一次都优先拿价值与体积比值最大的物体
c1 = v1/w1 = 1.125(最大)
c2 = v2/w2 = 1
c3 = v3/w3 = 1
c4 = v4/w4 = 1
所以优先拿第一个物体,随后背包再也装不下其他物体了,则最大价值为9。
但是这个问题的最优解是取物体2,3,4装进背包,最大价值为3+4+3=10!!!
所以这个问题不可以用贪心法来处理。

01背包转移方程:

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

i代表对i件物体做决策,有两种方式—放入背包和不放入背包。
j表示当前背包的容量。

理解方程:

image-20210110165435721如上图:以背包体积为0-8递增变化,依次放入物品;

当i=1,C=2时:
	此时背包体积正好等物物品1体积,可以放入物品1,此时背包内物品价值为1;即
	f[1][5] = f[i - 1][c - w[i]] + v[i] = 0 + 7
当i=1,C>2时:
	因为只放入物品1,此时背包内物品价值为1;即
	f[1][C] = f[i - 1][c - w[i]] + v[i] = 0 + 7
当i=2,C=2时:	
	此时背包体积只能放下物品1,此时背包内物品价值为1;即
	f[2][2] = f[i - 1][c] = 7
当i=2,C=7时:	
	此时背包体积只能放下物品1,此时背包内物品价值为1;即
	f[2][7] =  f[i - 1][c - w[i]] + v[i] = 7 + 5 = 12 
...
当i=3,C=8时:	
	f[3][8] = f[i - 1][c - w[i]] + v[i] = 7 + 10 = 17

总结:

不放入背包时: 第i次决策后的最大价值和第i-1次决策时候的价值是一样的(还是原来的那些物体,没多没少)。
放入背包时: 第i次决策后的价值为 第i-1次,背包容量为j - w[i]决策时候的价值 加上 当前物体的价值v[j]

设计函数:

import numpy as np

def solve(vlist,wlist,totalWeight,totalLength):
    resArr = np.zeros((totalLength,totalWeight+1),dtype=np.int32)

    for i in range(totalLength):
        for j in range(0,totalWeight+1):
	#俩个for循环的先后顺序不能变
            if wlist[i] <= j:
                resArr[i,j] = max(resArr[i-1,j-wlist[i]]+vlist[i],resArr[i-1,j])

            else:
                resArr[i,j] = resArr[i-1,j]
                
    return resArr[-1,-1]

if __name__ == '__main__':

    v = [7,5,10]
    w = [2,5,3]
    weight = 8
    n = 3
    result = solve(v,w,weight,n)
    print(result)

改进:

for i=1..N
  for j=V..0
  p[j]=MAX{p[j-volume[i]]+value[i],p[j]};
#改进2
def solve2(vlist,wlist,totalWeight,totalLength):

    resArr = [0] * (totalWeight+1) #只创建1个list,

    for i in range(totalLength):
        for j in range(totalWeight, -1, -1): #逆序!
		#俩个for循环的先后顺序不能变
            if wlist[i] <= j:
                resArr[j] = max(resArr[j],resArr[j-wlist[i]]+vlist[i])

    return resArr[-1]

#改进3
def solve3(vlist,wlist,totalWeight,totalLength):

    resArr = [0] * (totalWeight+1)

    for i in range(totalLength):
        for j in range(totalWeight, wlist[i]-1, -1): #简化循环
        #俩个for循环的先后顺序不能变    
        	resArr[j] = max(resArr[j],resArr[j-wlist[i]]+vlist[i])

    return resArr[-1]

if __name__ == '__main__':

    v = [7,5,10]
    w = [2,5,3]
    weight = 8
    n = 3
    result = solve(v,w,weight,n)
    print(result)

函数2:

def solve4(vlist,wlist,totalWeight,totalLength):

    #更改solve2两个for循环顺序
    for j in range(totalWeight, -1, -1):
        for i in range(totalLength):
    
            if wlist[i] <= j:
                resArr[j] = max(resArr[j],resArr[j-wlist[i]]+vlist[i])

    return resArr[-1]

if __name__ == '__main__':
    v = [60, 120,100,70]
    w = [15, 10,25,30]
    weight = 50
    n = 4

    # v = [7,5,10]
    # w = [2,5,3]
    # weight = 8
    # n = 3

    resArr = [0] * 51
    for i in range(n):
        v1 = [v[i]]
        w1 = [w[i]]
        result = solve4(v1,w1,weight,len(v1))
    print(result)

#实际应用场景, 见下面例子

0-1背包其他问题:

  1. 打印装入背包后价值最大的物品序号;
  2. 01背包还有一个问题,那就是满与不满。有时候让你求最大值,有时候会让你求恰好装满的最大值。这个时候该怎么办?我们看一下没要求恰好满的时候我们是如何初始化的,初始化价值都为0,是没有任何物品可以放入背包时的合法状态,是不存在非法状态的,所有的都是合法状态,因为可以什么都不装,这个解就是0。然而如果要求恰好装满的时候,只有容量为0的背包可能被价值为0的nothing“恰好装满”,初始化合法状态只有背包容量V为0的时候才可以初始化为0,其他的背包容量都要初始化为一个负无穷,对于二维的数组只要第一列初始化为0,一维的数组只要P[0]=0,其他都初始化负无穷(如果要求的是最小值,初始化为正无穷)。要注意的是改变初始化以后最后一个值是恰好装满的最大值,如果不能恰好装满,那肯定是一个负数(前提是初始化的负数足够大),而且对于恰好装满的的初始化情况的不要求满的最大值是0-v背包容量的最大值。即是最后一行的MAX。

例子:

题目描述

王强今天很开心,公司发给N元的年终奖。王强决定把年终奖用于购物,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件附件
电脑打印机,扫描仪
书柜图书
书桌台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。王强想买的东西很多,为了不超出预算,他把每件物品规定了一个重要度,分为 5 等:用整数 1 ~ 5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 j 件物品的价格为 v[j] ,重要度为 w[j] ,共选中了 k 件物品,编号依次为 j 1 , j 2 ,……, j k ,则所求的总和为:

v[j 1 ]*w[j 1 ]+v[j 2 ]*w[j 2 ]+ … +v[j k ]*w[j k ] 。(其中 * 为乘号)

请你帮助王强设计一个满足要求的购物单。

输入描述:

输入的第 1 行,为两个正整数,用一个空格隔开:N m

(其中 N ( <32000 )表示总钱数, m ( <60 )为希望购买物品的个数。)

从第 2 行到第 m+1 行,第 j 行给出了编号为 j-1 的物品的基本数据,每行有 3 个非负整数 v p q

(其中 v 表示该物品的价格( v<10000 ), p 表示该物品的重要度( 1 ~ 5 ), q 表示该物品是主件还是附件。如果 q=0 ,表示该物品为主件,如果 q>0 ,表示该物品为附件, q 是所属主件的编号) 

输出描述:

 输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值( <200000 )。

示例1

输入

2000 10
500 1 0
400 4 0
300 5 1
400 5 1
200 5 0
500 4 5
400 4 0
320 2 0
410 3 0
400 3 5

输出

7430

题目页面:https://www.nowcoder.com/practice/f9c6f980eeec43ef85be20755ddbeaf4?tpId=37&&tqId=21239&rp=1&ru=/ta/huawei&qru=/ta/huawei/question-ranking

难点:当主件存在附件时,需在4种情况种选1种 1、主件,2、主件+附件1,3、主件+附件2,4、主件+附件1+附件2,不一定每种情况都出现,只有当存在附件时才会出现对应的情况。

若存在附件需

n, m = map(int,input().split())
primary, annex = {}, {}
for i in range(1,m+1):
    x, y, z = map(int, input().split())
    if z==0:
        primary[i] = [x, y]
    else:
        if z in annex:
            annex[z].append([x, y])
        else:
            annex[z] = [[x,y]]
dp = [0]*(n+1)

for key in primary:
    w, v= [], []
    w.append(primary[key][0])#1、主件
    v.append(primary[key][0]*primary[key][1])
    if key in annex:#存在附件
        w.append(w[0]+annex[key][0][0])#2、主件+附件1
        v.append(v[0]+annex[key][0][0]*annex[key][0][1])
        if len(annex[key])>1:#附件个数为2
            w.append(w[0]+annex[key][1][0])#3、主件+附件2
            v.append(v[0]+annex[key][1][0]*annex[key][1][1])
            w.append(w[0]+annex[key][0][0]+annex[key][1][0])#4、主件+附件1+附件2
            v.append(v[0]+annex[key][0][0]*annex[key][0][1]+annex[key][1][0]*annex[key][1][1])
    
    #使用以下for循环顺序,当存在附件时,可以将有附件的几种情况,打印到dp列表中,参与判断。
    for j in range(n,-1,-10):#物品的价格是10的整数倍
        for k in range(len(w)):
            if j-w[k]>=0:
                dp[j] = max(dp[j], dp[j-w[k]]+v[k])

print(dp[n])

完全(无界)背包问题

有N种物品和一个体积为V的背包,每种物品都有无限件可用。第i件物品的体积是volume[i],价值是value[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包体积,且价值总和最大。

f[i][j] = max( f[i - 1][j], f[i - 1][j - k * w[i]] + k * v[i] )

简化:
for i=1..N
    for j=0..V
        p[j]=MAX{p[j-volume[i]]+value[i],p[j]};
为啥呢?因为之前的01背包要从V-0,是要找前一个i-1,因为每一个物品只有一个,放i物品的时候之前是没有i物品放入的。而现在不需要,因为每一个物品有很多次,这样我们就不必再找i-1,就找i就好了,我们允许放过i物品的基础上的背包再放一个i物品。即是
	p[i][j]=MAX{p[i][j-volume[i]]+value[i],p[i][j]};j是越来越大,相当于加入多个i物品。
	也可以这么理解。外层循环放0-V,对于每一个容量i我们要求出其能放的最大值。这样物品可以依次放如,循环的时候物品也可以重复放入。而且这两个循环是可以调换的,就变成了这样。

完全背包问题有一个很简单有效的优化:若两件物品i、j满足volume[i]<=volume[j]且value[i]>=value[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小体积高的j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据一件物品也去不掉。

多重背包

有N种物品和一个体积为V的背包。第i种物品最多有num[i]件可用,每件体积是volume[i],价值是value[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包体积,且价值总和最大。
这和01背包的区别就是多了一个num[i],这样我们可以把第i种物品分成num[i]份取出来,每份一件。这样就和01没有了区别了。可以变换数组,也可以直接增加一个循环,这样时间复杂度就增加了。值得注意的是虽然说是可以拿0 1 2 3 4 …但是要一件一件的拿,不是先拿1件,再拿2件,那就变成拿3件了!

参考:

https://blog.csdn.net/Iseno_V/article/details/100001133

https://blog.csdn.net/liangbopirates/article/details/9750463

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值