01背包,完全背包,多重背包,混合背包,二维费用背包,分组背包,背包问题求方案数

1 01背包问题

N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用 一次

i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

解析:

状态表示:

  • f[i][j] 表示只看前 i 个物品,总体积是 j 的情况下的最大价值。

状态转移:

  1. 不选第 i 个物品,f[i][j] = f[i-1]f[j] ,体积不变,与上一个物品的状态相等。

  2. 选第 i 个物品,f[i][j] = f[i-1][j-v[i]] + v[i],体积变小,加上当前物品的价值。

  • 所以转移方程为f[i][j] = max(1, 2)

边界:

  • f[0][j], f[i][0] = 0 ,体积或物品为 0 的情况下最大价值是 0
def func(v, w, m): # v是体积,w是价值,m是总体积
    f = [[0]*(m+1) for _ in range(n+1)]  # f[i][j]表示前i个物品,总体积是j的情况下的最大价值
    for i in range(1, len(w) + 1):  # 从第一个物品开始
        for j in range(1, m + 1):  # 体积从1开始
            f[i][j] = f[i - 1][j]  # 不选这个物品
            if j >= v[i - 1]:  # 若可以装下这个物品
                f[i][j] = max(f[i-1][j], f[i-1][j-v[i-1]] + w[i-1])  # 选这个物品
    print(f[n][m])
    # print(max([max(x) for x in f]))
  • 时间复杂度 O(n*V)
  • 空间复杂度 O(n*V)

存储空间优化

可以将所有的状态用一维数组表示,空间复杂度降为 O(V)

关键点在于当前使用的状态一定要是上一层的状态。即还未被修改为这一层的状态。

因为当前状态之和上一层的状态有关,所以可以优化为一维数组。首先看两个状态转移:

  1. f[i][j] = f[i-1][j]
  2. f[i][j] = f[i-1][j-v[i]] + w[i]

第一个状态:我们可以直接用 f[j] 代替。因为遍历到 f[j] 时,f[j] 还没有改变,依旧是上一层的状态。所以,我们不用再去判断何时能选,何时不能选。可以统一。

第二个状态:当遍历到状态f[j]时,因为j-v[j] < j,所以状态f[j-v[j]]已经遍历过了,是这一层的状态,我们想要的是上一层的f[j-v[j]]。所以体积可以从大到小遍历,这样遍历到f[j]时,f[j-v[j]]还没有遍历到,依旧为上一层的状态。

def func(v, w, m): # m为总重量
    f = [0]*(m+1)
    for i in range(1, len(w)+1):  # 物品[1, n]
        for j in range(m, v[i-1]-1, -1):  # 体积[m, v[i]]
            f[j] = max(f[j], f[j - v[i-1]] + w[i-1])
    print(f[m])

关于结果的优化表示

对于最后的结果可以直接输出 f[V],而不用遍历整个数组取最大值,是因为当我们 f[V] 全都初始化为0时, f[V] 表示体积小于等于 V 时可以获得的最大价值。

当我们只把 f[0] 初始化 0,其余的 f[i] 初始化 -INF,就能求出恰好体积为 V 时获得的最大价值。这样可以确保 f[V] 是从 f[0] 转移过来,从其他状态转移过来就是负无穷。

直观理解,当我们要求的是恰好体积为 V 时获得的最大价值。考虑这种情况,我们遍历体积是从1开始遍历到V,可其中有些体积并装不下任何物品,此时它们的状态应该是不存在的,且不应该转移到后面的状态。所以我们应该初始化为 -INF,这样状态转移到后面也是 -INF。

2 完全背包问题

N N N 种物品和一个容量是 V V V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

解析:

状态表示:

  • 我们利用优化后的一维数组来表示状态,f[i] 表示体积是 i 的情况下可以获得的最大价值。
  • res = max(f[0 ... m]),m 为总体积。

扩展01背包问题,v是体积,w是价值,m是总体积

def func(v, w, m):
    n = len(w) - 1
    f = [0]*(m+1)
    for i in range(1, n + 1):  # 物品[1, n]
        for j in range(m, v[i-1]-1, -1): # 体积 [m, 1]
            for k in range(1, j//v[i-1] + 1): # 个数 [1, j//v[i]]
                f[j] = max(f[j], f[j - k*v[i-1]] + k*w[i-1])
    print(f[m])

优化,只需将01背包中的第二个循环反向即可!

def func(v, w, m):
    f = [0]*(m+1)
    for i in range(1,len(w) + 1):  # 物品[1, n]
        for j in range(v[i-1], m+1): # 体积 [v[i], m]
            f[j] = max(f[j], f[j - v[i-1]] + w[i-1])
    print(f[m])

我们想一下,我们当初为何要逆序循环?

因为f[i][j]是由f[i-1][j-v]推导的,我们逆序循环正好保证了每个物品只被运用一次,但是如果我们正序循环,说明我们的f[i][j]是由f[i][j-v]推导出的,每个物品可以被运用多次。但这正是我们在完全背包里面想要的。

3 多重背包问题

有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

解析:

状态表示:

  • f[i] 表示体积是 i 的情况下可以获得的最大价值。

状态转移:

是01背包的扩展,01背包问题是每个物品只有选和不选两种情况。多重背包问题是可以不选,可以选一个,两个,直到 s i s_i si

  • f[j] = max(f[j], f[j-v[i]]+w[i], f[j-2*v[i]]+2*w[i])

01背包问题扩展

def func(v, w, m):
    f = [0]*(m+1)
    for i in range(1, len(w) + 1):  # 物品[1, n]
        for j in range(m, v[i-1]-1, -1): # 体积 [m, v[i-1]]
            for k in range(1, s[i-1] + 1): # 个数 [1, s[i]]
                if k*v[i-1] <= j:   # 确保背包装的下
                    f[j] = max(f[j], f[j - k*v[i-1]] + k*w[i-1])
    print(f[m])

时间复杂度 O(n^3)

优化方法一:二进制优化方法

  • 通过二进制优化转化为01背包问题。

给定任意一个数 s,最少可以把 s 分成多少个数,每个数选或不选,使得这些的可以组合为小于等于 s 的所有的数。

和用老鼠试毒药一个道理,可以分为 s 的二进制位的个数(log (s+1) 上取整)。选或不选分别代表对应二进制位上是否为 1。

7 = 111 拆为 1= 001, 2=010, 4=100,分别代表每个二进制为上为 1

0 = 都不选,1 = 选1,2=选2,3=选1和2,4=选4,5=选1和4,6=选2和4,7=选1和2和4.

**如果拆到最后剩余一部分值,则直接加入背包。**比如,13 = 1101, 则分解为 0001, 0010, 0100, 0110. 前三个数字可以组合成 7 以内任意一个数,每个数再加上0110 (= 6) 之后可以组合成任意一个大于等于 6 小于等于 13 的数,所以依然能组成任意小于等于 13 的数。

from collections import namedtuple

def func_1(v, w, s, m):  # v体积,w价值,s次数,m总体积
    Good = namedtuple('Good', ['v', 'w'])
    goods = [] # 存储所有物品
    f = [0] * (m + 1)
    # 把多个次数的物品二进制拆分
    for i in range(len(w)):
        k = 1
        while k <= s[i]:
            s[i] -= k
            goods.append(Good(v[i]*k, w[i]*k))
            k *= 2
        if s[i] > 0: goods.append(Good(v[i]*s[i], w[i]*s[i]))

    # 此时当作01背包来做
    for good in goods:
        for j in range(m, good.v - 1, -1): # 从大到小
            f[j] = max(f[j], f[j - good.v] + good.w)

    print(f[m])

优化方法二:单调队列优化方法

4 混合背包问题

有 N 种物品和一个容量是 V 的背包。物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

解析:

  • 将多重背包转换为01背包。
  • 然后分01背包和完全背包两个类别转移。
from collections import namedtuple

def func(v, w, s, m):  # v体积,w价值,s个数,m总体积
    Good = namedtuple('Good', ['kind', 'v', 'w'])
    f = [0]*(m+1)
    goods = []
    for i in range(len(w)):
        if s[i] < 0:  # 01背包
            goods.append(Good(-1, v[i], w[i]))
        elif s[i] == 0: # 完全背包
            goods.append(Good(0, v[i], w[i]))
        else:  # 多重背包转01背包
            k = 1
            while k <= s[i]:
                s[i] -= k
                goods.append(Good(-1, k*v[i], k*w[i]))
                k *= 2
            if s[i] >0: goods.append(Good(-1, s[i]*v[i], s[i]*w[i]))
    
    # 对不同类型的背包问题转移
    for good in goods:
        if good.kind == -1:  # 01背包
            for j in range(m, good.v-1, -1):
                f[j] = max(f[j], f[j - good.v] + good.w)
        else:  # 完全背包
            for j in range(good.v, m+1):
                f[j] = max(f[j], f[j - good.v] + good.w)
    print(f[m])

5 二维费用背包问题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。

解析:

f[i][j] 表示体积为 i ,重量为 j 时的背包最大价值。

先枚举物品,再枚举体积,再枚举重量。因为是01背包,所以体积和重量从大到小枚举。

def func(v, w, m, V, M): #v体积,w价值,m重量,V总体积,M总重量
    f = [[0]*(M+1) for _ in range(V+1)]
    for i in range(1, len(w)+1):  
        for j in range(V, v[i-1]-1, -1):
            for k in range(M, m[i-1]-1, -1):
                f[j][k] = max(f[j][k], f[j-v[i-1]][k-m[i-1]] + w[i-1])
    print(f[V][M])

6 分组背包问题

有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。

每件物品的体积是 vij,价值是 wij,其中 ii是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

解析:

状态转移

for i in [1, n]:  # 物品总数
	for j in [m, v]: # 体积从大到小枚举 
		f[j] = max(f[j], f[j-v[0]]++w[0], f[j-v[1]]++w[1], ...)

最后一层的意思是这一组的物品,可以都不选,可以选第一个,可以选第二个,第一选第三个…,取最大。

def func(v, w, m): # v分组体积[[v1. v2],  [v1]], w分组价值[[w1, w2], [w1]],m总体积
    f = [0]*(m+1)
    for i in range(len(w)):  # 枚举每一个组
        for j in range(m, -1, -1): # 体积 [m, 0]
            for k in range(0, len(v[i])): # 个数 
                if j >= v[i][k]:
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k])
    print(f[m])

7 背包问题求方案数

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 10^9+7 的结果。

解析:

f[i] 表示体积恰好为 i 的情况下背包的最大价值。

g[i] 表示体积为 i 的情况的方案数。

  • 若当前最大值等于拿与不拿其中一种决策,那么g[i] 就等于其中一种的方案数
  • 若当前最大值等于拿与不拿两种决策,那么g[i]就等于两种的方案数之和
    注意 f 的所有初始化除了f[0] 都要设为 -INF,我们要求恰好体积为 i 的情况的最大价值。不然小于等于体积i的最大价值,不太好统计方案数。
def func(v, w, m): # m为总重量
    f = [float('-inf')]*(m+1)
    g = [0]*(m+1)
    f[0] = 0
    g[0] = 1  # 体积为0的方案数为1
    for i in range(1, len(w)+1):  # 物品[1, n]
        for j in range(m, v[i-1]-1, -1):  # 体积[m, v[i]]
            t = max(f[j], f[j - v[i-1]] + w[i-1])  # 取出最大值
            s = 0
            if t == f[j]: s = (s+g[j])%mod  # 如果最大值等于f[j],那么加上g[j]
            if t == f[j - v[i-1]] + w[i-1]: s= (s + g[j - v[i-1]])%mod  # 如果最大值等于f[j-v[i-1]] + w[i-1],那么加上g[[j-v[i-1]]
            f[j], g[j] = t, s
    maxw = max(f)
    res = 0
    for i in range(len(f)):
        if f[i] == maxw:
           res = (res + g[i])%mod
    print(res)

8 求背包问题的方案

9 有依赖的背包问题

物品之间有依赖。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值