动态规划-背包问题

背包问题算是动态规划里的基础问题了,但是并不是背包问题就很简单。彻底明白其中的原理,是我们理解动态规划算法的基础,下面的总结基本来至《背包问题九讲》。总结的过程总是能促进我们自己的思考,希望每个努力提高自己的小伙伴儿都能攻下心中的堡垒。

1. 01背包问题

题目
有 件物品和一个容量为 的背包。放入第 件物品耗费的费用是 (也即占用背包的容量),得到的价值是 。求解将哪些物品装入背包可使价值总和最大。

分析求解
首先定义状态: 表示前 件物品放入容量为 的背包能得到的最大价值。

当我们考察第 件物品的时候,前 件物品已经确认,我们此时只需要考虑对第 件物品进行操作:装入背包或者不装入背包。
当我们选择不装入背包时,则有 ,即前 件物品装入背包中的最大价值为 ;当我们选择装入背包时,则有 ,因为我们此时装入了第 件物品,它耗费的费用为 ,所以没有装入前背包容量为 。

因为 表示的是前 件物品放入容量为 的背包能得到的最大价值,所以我们需要计算装与不装两种情况下的最大值,即

这便是我们的状态转移方程。
伪代码如下:

F[0,…,V] = 0
for i in 1 to N:
    for v in Ci to V:
        F[i, v] = max(F[i-1, v], F[i-1, v-Ci] + Wi)

状态压缩
接下来考虑对状态进行压缩。由上面的状态转移方程,当我们计算 时,需要取用 和 的值。如果我们只用一个一维数组 来存储这个值是否能满足要求呢?
我们需要保证更新 时, 保存的是 的值,所以我们可以在内循环中由大到小的顺序来更新:

F[0,…,V] = 0
for i in 1 to N:
    for v in V to Ci:
        F[v] = max(F[v], F[v-Ci] + Wi)

我们再内循环抽象成函数,写成如下形式:

def ZeroOnePack(F, C, W):
    for v in V to C:
        F[v] = max(F[v], F[v-C]+W)

F[0,…,V] = 0
for i in 1 to N:
    ZeroOnePack(F, Ci, Wi)

复杂度
时间复杂度:,即两重循环。
空间复杂度: ,即 占用的空间

初始化
对于 的初始化问题,需要考虑题目要求,若是说需要装满背包,则除了 外,其他的需要定义为 ,因为初始时背包中未装物品,都为非法状态;如没有说要装满背包,则全部定义为 。

2. 完全背包问题

题目
有 种物品和一个容量为 的背包,每种物品都有无限件。放入第 件物品耗费的费用是 (也即占用背包的容量),得到的价值是 。求解将哪些物品装入背包可使价值总和最大。

分析求解
此问题和 01背包问题非常相似,不同的是物品有无限件,现在就不是选与不选的问题,而是选取多少件(0件、1件、…、件)的问题。我们仍然用01背包问题的思路来求解:

此时我们需要求解的状态数仍然是 ,但是求解每一个状态的时间复杂度不再是 ,而是 ,总的时间复杂度为 。

此外,我们也可以将每种物品看做 件价值和费用都相同的物品,然后完全套用01背包问题进行求解。更加高效的方式是将第 种物品拆分成费用为 、价值为 的物品,其中 为满足 的非负整数。

再进一步:在01背包问题中,我们的状态转移方程为

即我们考察第 件物品的时候,依据的是前 件装选背包的状态。而此时我们第 种物品有无限件,当我们考察第 种物品的时候,就有可能是“加选一件第 种物品”,于是这里就存在 “加选” 与 “不加选” 这两种情形,所以此时我们的状态转移方程为

同样对状态进行压缩:

需要注意的是,此时的 保存的是 的值,所以我们的内循环要采用递增的方式:

F[0,…,V] = 0
for i in 1 to N:
    for v in Ci to V:
        F[v] = max(F[v], F[v-Ci] + Wi)

我们再将内循环抽象成函数,写成如下形式:

def CompletePack(F, C, W):
    for v in C to V:
        F[v] = max(F[v], F[v-C]+W)

F[0,…,V] = 0
for i in 1 to N:
    CompletePack(F, Ci, Wi)

3. 多重背包问题

题目
有 种物品和一个容量为 的背包,第 种物品最多有 件物品可用。放入第 件物品耗费的费用是 (也即占用背包的容量),得到的价值是 。求解将哪些物品装入背包可使价值总和最大。

分析求解
对比完全背包问题,多重背包问题每种物品的件数有限制,当用01背包问题求解思路时,状态转移方程可写成

因为求解每个状态的时间复杂度为 ,所以总的时间复杂度为 。

和完全背包问题类似,我们还可以考虑将每种物品看做 件费用和价值都相等的物品来应用01背包问题的求解方法;此外,利用二进制的思想,同样可以将第 种物品拆分成系数为 的几件物品,其中 是满足 的最大整数。

这就将第 种物品分成了 种物品,也将原问题的复杂度转化成了 。

def MultiplePack(F, C, W, M):
    if C*M >= V:
        CompletePack(F, C, W)
        return
    k = 1
    while k < M:
        ZeroOnePack(F, k*C, k*W)
        M = M - k
        k = 2 * k
    ZeroOnePack(F, M*C, M*W)

若题目的问题是”这些若干件的物品能否填满给定容量的背包“,这里只考虑背包容量,而不考虑物品价值,那还能得到时间复杂度为 的算法。
我们将状态 定义为 用前 件物品填满容量为 的背包后还剩下几个第 种物品可用,如果 ,则表明状态不可用,若可行应满足 。

F[0,…,V] = -1
F[0,0] =  0
for i in 1 to N:
    for j in 0 to V:
        if F[i-1, j] >= 0:
            F[i, j] = Mi
        else:
            F[i, j] = -1
    for j in 0 to V-Ci:
        if F[i, j] > 0:
            F[i, j+Ci] = max(F[i, j+Ci], F[i][j]-1)

4. 混合3种背包问题

我们只需要在对每件物品考察前,判断其数量,然后分别处理即可。

F[0,…,V] = 0
for i in 1 to N:
    if 第 i 件物品属于 01 背包:
        ZeroOnePack(F, Ci, Wi)
    elif 第 i 件物品属于 完全背包:
        CompletePack(F, Ci, Wi)
    elif 第 i 件物品属于 多重背包:
        MultiplePack(F, Ci, Wi, Mi)

5. 二维费用的背包问题

题目
与前面背包问题不同的是,二维费用主要是指每种物品有两种不同的费用(背包容量)。
定义第 种物品所需的两种费用为 和 ,两种费用可付出的最大值(背包容量) 分别为 和 ,物品的价值为 。

分析求解
我们只需要在前述基础上,将状态增加一维即可。设 为前 件物品分别付出费用为 和 时可获得的最大价值,则状态转移方程为:

同样可以考虑对状态进行压缩。若是01背包问题,内循环时需要递减的顺序;若是完全背包问题,内循环需要递增的顺序;多重背包问题时,同样对物品进行拆分处理。

物品总件数的限制
二维费用问题通常是以一种隐含的方式给出的:最多只能选择 件物品。其实这相当于多了一种 "件数" 的费用,每个物品的件数费用均为1,可以付出的最大件数费用的 ,即 表示付出费用为 ,最多选择 件时的最大价值。

6. 分组背包问题

题目
有 件物品和一个容量为 的背包。放入第 件物品耗费的费用是 (也即占用背包的容量),得到的价值是 。这些物品被划分为 组,每组中的物品互相冲突,最多只能选一件,求解将哪些物品装入背包可使价值总和最大。

分析求解
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。设 表示前 组物品花费费用 能取得的最大权值,则有

同样可进行空间复杂度的优化,伪代码如下:

for k in 1 to K:
    for v in V to Ci:
        for all item i in group k:
            F[v] = max(F[v], F[v-Ci]+Wi)

这里三层循环的顺序保证了每一组内的物品最多只有一个会被添加到背包中。

7. 有依赖的背包问题

题目
这里的依赖定义为选择物品 ,则必须选物品 ,也称为物品 依赖于物品 。我们假设没有某个物品既依赖于别的物品,又被别的物品所依赖,另外,没有某件物品同时依赖多件物品。

分析求解
我们将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。我们考虑一个主件和它的附件集合,选择策略有:一个都不选、选择主件和一个附件、选择主件和2个附件……这样对于有 个附件的集合,那我们的选择策略有 种。

值得关注的是,这些策略都是互斥的,我们可以将某个主件和它的附件集合看做一个分组,利用 6 中的分组背包问题求解,但遗憾的是选择策略并没有减少,依然是指数级。

对于第 个物品组中的物品,如有费用相同的物品,则可以只保留一个价值最大的物品,这算是一个小优化。此外,对于主件 的附件集合,我们可以应用一次01背包算法,得到费用依次为 所有这些值时相应的最大价值 ,这样就可以将主件 及它的附件集合看做为 个物品的物品组,其中费用为 的物品的价值为 , 的取值范围为 ,接下来再应用 6 中的分组背包算法求解。

更一般的情况
更通常的情况是主件的附件依然存在附件,但是一般都会限制每个物品最多只依赖一个物品,且不会出现循环依赖。
解决这类问题,只能从最底层开始,先将附件转换成物品组,在一层一层向上求解。

8. 泛化物品

泛化物品没有固定的费用和价值,它的价值随着分配给它的费用而发生变化。
在背包容量为 的背包问题中,泛化物品是一个定义域为 中整数的函数 ,当分配给它的费用为 时,能得到的价值就是 。

一个费用为 ,价值为 的物品,若它是01背包中的物品,则可以看做是 ,其余函数值都为 的泛化物品;若是完全背包问题,则其价值函数为 ,除 为整数值外,其余值都为 ;若是件数为 的多重背包问题,则 ,当且仅当 为整数且 时有价值,其余情况均为 。

一个物品组可以看做一个泛化物品 ,对于一个 中的 ,若物品组不存在费用为 的物品,则 ,否则 取值为所有费用为 的物品的最大价值。对于 7 中的每个主件和其附件集合,就可以看做是一个泛化物品。

泛化物品的和
若给定了两个泛化物品 和 ,给定费用为 的情况求其最大价值,则我们只需要枚举将这个费用如何分配给两个物品即可。对于 中的每一个整数 ,最大价值 即为:

这样看来, 也可以看做是 和 决定的泛化物品, 定义为泛化物品 和 的和。所以我们的背包问题,也都可以看做是泛化物品的求和问题。

9. 背包问题问法的变化

一般的背包问题,都是在限制容量的情况下求解最大价值,类似的问题还有最多可以装多少件物品,最多可以装满多少空间的背包等,这类问题都可以根据最终的状态数组 求得。对于“最小价值” 和 "最少件数" 的问题,则只需要将状态转移方程中的 max 改成 min 即可。下面总结下其他变相问法。

输出方案
问题需要我们给出最终选择的物品集合。对于这类问题,我们在求解过程中需要记录状态转移的过程,即每个状态的最优值是由状态转移方程的哪一项推出来的。

以01背包问题为例,状态转移方程为 ,我们定义一个数组 , 表示推出 的值时采用了方程的前一项(), 表示推出 的值时采用了方程的后一项(),则最后输出方案为:

i = N
v = V
while i > 0:
    if G[i, v] == 0:
        print(未选择第 i 件物品)
    if G[i, v] == 1:
        print(选择第 i 件物品)
        v = v - Ci
    i = i - 1

当然我们也可以不采用 ,而是利用 进行推算:将 改为 ,将 改为 即可。

求方案总数
由于状态转移方程考虑了所有可能的背包组成方案,所以我们只需要将状态转移方程中的 max 改成 sum 即可。

求最优方案总数
以01背包问题为例,我们定义一个数组 来同步求解最优方案的总数

G[0, 0] = 1
for i in 1 to N:
    for v in 0 to V:
        F[i, v] = max(F[i-1, v], F[i-1, v-Ci]+Wi)
        if F[i, v] == F[i-1, v]:
            G[i, v] = G[i, v] + G[i-1, v]
        if F[i, v] == F[i-1, v-Ci]+Wi
            G[i, v] = G[i, v] + G[i-1, v-Ci]

求次优解、第K优解
以01背包问题、求解第 优解为例进行说明。要点在于将每个状态都表示成有序队列,状态转移方程中的 max 改成有序队列的合并。

对于01背包问题的状态转移方程 ,我们需要求解第 优解,则状态 就为一个大小为 的队列 , 就表示前 个物品中,背包大小为 时,第 优解的值。当进行合并时,我们只需要将 和 的前 大值存入到 中即可,总的时间复杂度为 。

你可能感兴趣的:(动态规划-背包问题)