【python】一篇讲透背包问题(01背包 完全背包 多重背包 二维费用背包)

面对背包问题,有一个很重要的方程式:状态转移方程式
所以每一种背包问题我都会给出状态转移方程式

#01背包

什么是01背包型问题?

先给大家感受一下01背包型问题:
给定n种物品和一背包。物品i的重量是wi,其价值为ci,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
这种时候面对每一个物品都有两个选择:选还是不选,这就是典型的01背包问题!

01背包怎么做?

既然01背包的核心是拿与不拿,那么就要判断是拿之后总价值高还是不拿的总价值高。当然,拿是有前提的,那就是背包的容量要够!

背包容量够了,那如何看拿与不拿谁的总价值高呢?
我们先来看下dp表,就可以一目了然!
在这里插入图片描述
图片中的 1 是背包容量为(5-3)的最大价值,5是当前背包容量,为了在书包里边放下2号物品(也就是所需容量为 3 ),所以要给该物品腾出 3 的位置。

根据上面的推算,得出01背包状态转移方程式
dp[i][j] = max( dp[ i - 1 ][ j ] , dp[ i - 1 ][ j - w[ i ]] + c[ i ])

实战

T大计算机系费尽九牛二虎之力,终于挖来了某系第一肌肉男(从此被称为计算机第0肌肉男)。他平时除了自习,就是看电影。但是即使肌肉男有强大的肌肉,仍然会面临每个月有限的流量耗尽的问题。已知肌肉男的流量p是有限的,现在有某舍友提供的n部最新电影,看每部电影会消耗肌肉男一定的流量,但是也会给肌肉男带来一定的兴奋值。肌肉男想在自己有能力的情况下产生最多的兴奋值,他由于平时忙于上自习,把这个任务交给了你,如果你能完成,他就帮你领一年的外卖哦!
输入数据第一行有一个数字p,表示肌肉男的流量值;第二行是一个数字n,表示一共有n部电影;以下有n行,每行第一个数表示该电影消耗的流量大小,第二个数表示该电影带给肌肉男的兴奋值。
输出数据只有一行,表示肌肉男所能达到的最大兴奋值。
样例输入
10
4
2 1
3 3
4 5
7 9
样例输出
12

p = int(input())
n = int(input())
lists = [list(map(int,input().split())) for i in range(n)]  # lists[0]为所需流量 lists[1]为兴奋值
# print(lists)
dp = [[0]*(p+1) for _ in range(n+1)]
for i in range(1,n+1):
    for j in range(1,p+1):
        if lists[i-1][0] <= j :
            dp[i][j] = max(dp[i-1][j],dp[i-1][j-lists[i-1][0]]+lists[i-1][1])
        else:
            dp[i][j] = dp[i-1][j]

print(dp[n][p])
优化算法

看完上边的推理,你会发现,其实每一次的推算,都只使用到上一行的值,用不到整个dp表,那么我能不能把dp表缩减到只有一行呢?

那这个时候,状态转移方程式就变成:
dp[j] = max( dp[ j ] , dp[ j - w[ i ]] + c[ i ])

可是,这样的话,我们要的dp[j-w[i]]总是会被覆盖掉,拿不到我们想要的值。
采用倒推的方式,就可以解决掉这个问题!

# 优化
# dp算法有后无效性原则,01背包每次只看前一行,前一行之前的那些都是没用的,那我就没必要留着
p = int(input())
n = int(input())
lists = [list(map(int,input().split())) for i in range(n)]  # lists[0]为所需流量 lists[1]为兴奋值
# print(lists)
dp = [0 for i in range(p+1)]
for i in range(1,n+1):
    for j in range(p,1,-1):    # 注意:这里是逆序
        if lists[i-1][0] <= j :
            dp[j] = max(dp[j],dp[j-lists[i-1][0]]+lists[i-1][1])

print(dp[p])

#完全背包

什么是完全背包型问题?

完全背包问题,顾名思义就是01背包问题的进阶。01背包中每一个物品只有拿与不拿两种选项,完全背包中则多了一个拿多少的问题,也就是说完全背包中物品的数量是不限的。

完全背包怎么做?

物品的数量真的不限吗?
物品的数量受限于背包的容量大小!
那么也就是说,只要在01背包的基础上,加多一个循环,循环拿物品的数量,就好了!

状态转移方程式:dp[ j ] = max(dp[ j ] , dp[ j - k * w[ i ]] + k * c[ i ])

实战

题目描述
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
输入
第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);
第2…N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
输出
仅一行,一个数,表示最大总价值。
样例输入
10 4
2 1
3 3
4 5
7 9
样例输出
max=12

m, n = map(int, input().split())
w = [0 for _ in range(n + 1)]
c = [0 for _ in range(n + 1)]
for i in range(1, n + 1):
    w[i], c[i] = map(int, input().split())

dp = [0 for i in range(m + 1)]
for i in range(1, n + 1):
    for j in range(m, 1, -1):
        for k in range(0, j // w[i] + 1):
            if w[i] * k <= j:
                dp[j] = max(dp[j], dp[j - k * w[i]] + k * c[i])
print('max=' + str(dp[m]))
优化算法

又加一层循环,也就意味着这个算法有三重循环,太过冗长了!优化走起!
说了这么多,不如一张表来的实在:
在这里插入图片描述
看完表,你会发现,其实就是看书包还能不能再装多一个,能装的话就对比装的价值高还是不装高,对比的对象就是dp[ i ][ j - w[ i ]],为什么不是dp[ i - 1][ j - w[ i ]]?
因为这个物品是可以无限拿的,dp[ i ][ j - w[ i ]] 是你在能拿当前物品下的最优解,而dp[ i - 1][ j - w[ i ]]不能拿该物品。

那么状态转移方程式就会变成:dp[j] = max( dp[ j ] , dp[ j - w[ i ]] + c[ i ])

# 优化算法
m, n = map(int, input().split())
w = [0 for _ in range(n + 1)]
c = [0 for _ in range(n + 1)]
for i in range(1, n + 1):
    w[i], c[i] = map(int, input().split())

dp = [0 for _ in range(m + 1)]
for i in range(1, n + 1):
    for j in range(1, m + 1):  # 注意:这里是正序
        if w[i] <= j:
            dp[j] = max(dp[j], dp[j - w[i]] + c[i])
print('max=' + str(dp[m]))
对比

眼尖的你一定会发现,这状态转移方程式怎么和01背包的一模一样!
01背包是与上一行作比较,为了不覆盖掉上一行的数据,要采用倒推的形式。
完全背包是与同一行作比较,所以前面的数据要先获得,要采用顺推的形式。

#多重背包

什么是多重背包型问题?

多重背包问题与完全背包很像,不同在于物品的数量是有限的。

多重背包怎么做?

完全背包是无限的,多重背包是有限的。那不就是在第三重循环的时候把数量改成有限就好了。
状态转移方程式:dp[ j ] = max(dp[ j ] , dp[ j - k * w[ i ]] + k * c[ i ])

实战

题目描述
有n种物品,小王有一个能装m千克的背包,想要装点物品回去。
每种物品,有自己的重量w(千克)和价值v(元),以及他们的数量c。
现在,物品的数量很大,而种类也不少。
请你计算出,背包装的最大价值。
输入
第一行是整数n(n<=100),m(m<=3000),表示物品的种类和背包容量。
接下来n行,每行3个数,w(w<=100),v(v<=100),c(c<=1000),表示重量,价值,和数量.
输出
一个数,表示最大价值。
样例输入
3 50
5 5 20
3 3 30
30 60 5
样例输出
80

n, m = map(int, input().split())
w = [0 for i in range(n + 1)]
v = [0 for i in range(n + 1)]
c = [0 for i in range(n + 1)]
for i in range(1, n + 1):
    w[i], v[i], c[i] = map(int, input().split())

dp = [0]*(m+1)
for i in range(1, n + 1):
    for j in range(m,0,-1):
        for k in range(0, c[i]+1):
            if k * w[i] <= j:
                dp[j] = max(dp[j],dp[j - k * w[i]] + k * v[i])
            else:
                break
print(dp[-1])
优化算法

在完全背包的时候讲过,这种算法太过冗长,会发生超时的情况,就这道题目来看,这个算法只拿到了83分,还不是一个完美的答案。
那么我们就对他稍作修改吧~

n, m = map(int, input().split())
dp = [0 for i in range(m + 1)]

for _ in range(n):
    w, v, c = map(int, input().split())
    t = 1
    while c >= t:    # 还能拆
        for j in range(m, w * t - 1, -1):  # 倒推 
            dp[j] = max(dp[j], dp[j - w * t] + t * v)
        c -= t
        t *= 2
    if c:
        for j in range(m, w * c - 1, -1):
            dp[j] = max(dp[j], dp[j - c * w] + c * v)
print(dp[-1])

这种算法采用的是二进制的思想,把1000个拆分成 1,2,4,8,16,32,64,128,256,480
十个物品,他们所对应的价值就是 k * v
这样就从循环1000次缩小到10次,大大的压缩了时间!

为什么要采用二进制:

假设有50个苹果,现在要取n个苹果,如何取?

最简单的方法是将苹果分成6份,分别是1、2、4、8、16、19个,因为这样你会发现,无论你想要多少个,都可以直接拿走某一份或多份,而不用再去一个个的数。

#二维费用背包

什么是二维费用背包型问题?

顾名思义,就是他会有两种费用,比01背包多一层费用。01背包只有容量的限制,二维背包会多一个体积的限制。

二维费用背包怎么做?

既然是二维化的01背包,那就把01背包的dp数组二维化就好了。
状态转移方程式:dp[ j ][ k ] = max(dp[ j ] [ k ], dp[ j - a[ i ]][ k - b [ i ]] + c[ i ])

明天接着更,今天假期就先写到这~

状态转移方程
后无效性原则
吃水不忘挖井人,这是原视频>

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值