[动态规划] 一文搞定三类背包问题(超基础详细)


动态规划问题一直是算法中的难点和重点之一,而三类背包问题使用的动态规划思想本质上是一样的,下面我会通过背包问题解释如何用动态规划求解,如果觉得文字太啰嗦可以直接看代码。

01背包问题

有n个物品,每个物品有体积和价值两个属性,现在有一个固定容量的背包,要将这些物品放入背包内,使得背包内物品总价值最大,问要如何放?

注意:这里每个物品的数量为1。

设$V = 14$。

为了描述方便起见,我们用$c_1,c_2,c_3,....c_n$表示物品的体积, 用$w_1,w_2,w_3,....w_n$表示物品的价值,用$V$表示背包的容量,$n$表示物品的数量。

动态规划描述

背包问题是动态规划问题的一种,和分治算法一样,它是将一个大问题分成一个一个的小问题,然后找到大问题和小问题之间的递推式,将一个个小问题组合成大问题,

并且通常要求这些小问题也要达到最优。能用动态规划解决的问题一般含有两种性质:最优子结构重叠子问题。动态规划从本质上讲就是穷举法, 但是它的却比一般的算法效率要高,这就是因为它能剪去重叠的子问题,使得总体复杂度不那么高。

 

解决方法

1.递归方法

我们先从最容易入手的地方开始,用递归的方法来解这个问题。

首先对于每个物品,我们的选择只有两个:放或者不放。我们将所有的可能都穷举出来,就可以得到下面这个树状图(只画了前四个结点):

这个树上每一个分支都只能选一个,每一行就代表一个子问题。我们用$F(i, v_i)$表示前$i$个物品放入背包内的最大价值(即最优方案),此时这个$v_i$指的是放入前$i$个物品后背包剩余的容量。

显而易见$F(5, v_5)$就是我们要解的大问题,分割一下子问题就是:$F(1, v_1)$, $F(2, v_2)$, $F(3, v_3)$, $F(4, v_4)$。要解决这个大问题,我们就得先解决它的前面一个子问题$F(4, v_4)$(求解原问题时子问题也要达到最优,所以该问题具有最优子结构,这是判断问题能否用动态规划解的条件之一)。

至于为什么解决$F(5, v_5)$问题之前要先解决$F(4, v_4)$,这是因为对于问题$F(5, v_5)$来说,如果解决了问题$F(4, v_4)$剩下就只需要判断最后一个物品体积是否比剩余背包容量大,是直接放入背包,否则就不放。

而对于子问题$F(4, v_4)$,我们得解决$F(3, v_3)$....从而得解决$F(1, v_1)$。对于子问题$F(1, v_1)$,由于它前面没有子问题,所以解决它只需要判断$c_1 \le V$是否成立。

所以对于每一个子问题,由于前面的子问题已被解决,因此我们都只需要做两个选择:放,还是不放。

假设我们已经知道了前$i-1$个物品放入背包的最优方案$F(i-1, v_{i-1})$,那么对于第$i$个物品要放入背包就有三种情况:

   若物品的体积$c_i$大于背包剩余的容量$v_{i-1}$,那么只能丢弃这个物品:

     $F(i, v_i) = F(i-1, v_{i-1})$

   否则就有两种选择:

   1. 不放第$i$个物品:

    $F(i, v_i) = F(i-1, v_{i-1})$

   2. 放第$i$个物品:

    $F(i, v_i) = w_i + F(i-1, v_{i-1}-c_i)$

要从这两个方案中选择总价值最大的,所以:

$$
F(i, v_i) =
\begin{cases}
F(i-1, v_{i-1}), & \text{ $c_i > v_{i-1}$ } \\
max(F(i-1, v_{i-1}), w_i +F(i-1, v_{i-1}-c_i)), & \text{ $c_i \le v_{i-1}$ }
\end{cases}
$$

 用Python实现代码如下:

#递归求解
def rec_bag(c, w, v, i=0):
    '''
    param c: 物品体积
    param w: 物品价值
    param v: 当前背包剩余容量
    param i: 当前物品编号
    return: 背包装下物品的最大价值
    '''
    if i > len(c)-1:
        return 0
    elif v <= 0: #体积不能为负
        return 0
    elif v > 0:
        if c[i] <= v:
            A = w[i] + rec_bag(c, w, v-c[i], i+1)
            B = rec_bag(c, w, v, i+1)
            res = max(A, B)#两种方案中选最优的那个并返回
        else:
            res = rec_bag(c, w, v, i+1)#物品体积大于背包容量,直接返回
    return res

 

2.动态规划方法

递归方法最大的缺点就在于有较多重复的计算,通过上面的树状图可以知道,递归会遍历这棵树的所有结点,所以它的时间复杂度为:$O(2^{n})$。

指数阶的时间复杂度对于计算机来说简直就是灾难!那有没有什么办法减少这些重复的计算呢?这就是接下来要说的动态规划,它可以通过存储已经计算出来的结果来减少重叠子问题。

和递归一样,动态规划也是要找出所有可能的情况并从中选出最优的,但不同的是动态规划每次计算都会将结果存储在一个表中,下一次遇到同样的问题时就直接查表,而不必重复计算。

为了方便描述,我们重新将$F(i, V)$定义为前$i$个物品在总容量为$V$背包里的最大价值(注意和这里的$V$指的是背包总容量并非剩余容量)。那么现在的大问题就是$F(5, 14)$,按照上面递归的思想,改写一下式子就是:

$$
F(i, V) =
\begin{cases}
F(i-1, V), & \text{ $c_i > V$ } \\
max(F(i-1, V), w_i +F(i-1, V-c_i)), & \text{ $c_i \le V$ }
\end{cases}
$$

对于动态规划通常我们会用自底向上的方法,而递归是自顶向下的。自底向上的意思就是我们从子问题$F(0, 0)$一直计算到$F(5, 14)$,我们知道要计算$F(5, 14)$,得先解决它前面的子问题,但由于是自底向上的,所以我们并不知道$F(5, 14)$的子问题有哪些,因此我们只能够把它所有可能的子问题都找出来,即$F(4, V), (V \in (0, 1, 2, 3......14))$,而$F(5, 14)$的子问题一定是包含在内的,比如$F(5, 14)$的子问题就是$F(4, 14)$和$F(4, 14-c_5)$。所以自顶向下的过程就相当于在填这个表:

表一旦填完,答案也就出来了。

#动态规划求解
def dp_bag(c, w, v):
    res = [[0 for j in range(v+1)]for i in range(len(c)+1)] #构建一个大小为(len(c)+1)*(v+1)的表
    for i in range(1, len(c)+1): #i当前表示物品编号
        for j in range(1, v+1): #j表示背包容量
            if j<c[i-1]:
                res[i][j] = res[i-1][j]
            else:
                res[i][j] = max(res[i-1][j-c[i-1]] + w[i-1], res[i-1][j])
    return res[len(c)][v]

算法的时空复杂度都是:$O(n \times V)$,并且如果$V$足够小,可进一步降低至线性($O(n)$),比递归方法要高效地多。

空间复杂度优化:根据递推式可知,第$i$个子问题只与第$i-1$个子问题有关,因此我们用两个列表来存储第$i$个和第$i-1$个子问题,每当前面一个子问题求解完,我们就用另一个列表来存下一个子问题。

def dp_bag_opt(c, w, v):
    res = [[0 for j in range(v+1)] for i in range(2)] #只需要创建一个大小为2*(V+1)的表
    res1, res2 = res[0], res[1]
    for i in range(1, len(c)+1): #i当前表示物品编号
        for j in range(1, v+1): #j表示背包容量
            if j<c[i-1]:
                res1[j] = res2[j]
            else:
                res1[j] = max(res2[j-c[i-1]] + w[i-1], res2[j])
        res1, res2 = res2, res1
    return res2[-1]

此时空间复杂度为$O(2V)$。

 

完全背包问题

这里还是以01背包的例子为例,除了每个物品可取放的数量是无限的,其他条件不变。现在对于每件物品来说,能做的选择变多了,之前要么放要么不放;现在是不放和放多少个。也就是说递推式现在应该有多个选择,对于递归解法,根据上面01背包递归解法的递推式,这里要能使得$F(i, V)=w_i +F(i-1, V-c_i)$对于每个物品都能重复进行多次,而不是一次就结束了;对于动态规划解法,在一个子问题$F(i, j)$内(即第二层for循环里),可以设计一个for循环来计算在这个容量下最多可以放几个同类的物品。

用Python实现的代码如下:

#递归求解
def rec_complete_bag(c, w, v, i=0):
    if i > len(c)-1:
        return 0
    elif v <= 0: 
        return 0
    elif v > 0:
        if c[i] <= v:
            A = w[i] + rec_complete_bag(c, w, v-c[i], i)#一直递归地选择A情况,直到背包容量小于0
            B = rec_complete_bag(c, w, v, i+1)
            c = max(A, B)
        else:
            c = rec_complete_bag(c, w, v, i+1)  
    return c

#动态规划求解
def dp_complete_bag(c, w, v):
    res = [[0 for j in range(v+1)]for i in range(len(c)+1)]
    for i in range(1, len(c)+1):
        for j in range(1, v+1):
            if j<c[i-1]:
                res[i][j] = res[i-1][j]
            elif j>=c[i-1]:
                A = -1
                t = j
                k = 1
                while t>=c[i-1]:
                    t -= c[i-1]
                    A = max(A, k * w[i-1] + res[i-1][t])#计算多个同类物品叠加的价值
                    k += 1 
                res[i][j] = max(A, res[i-1][j])#还要算上不放的情况
    return res[len(c)][v]

动态规划解法我没有优化空间,可以自行用上面的方法进行优化。

 

多重背包问题

问题描述:与完全背包不同的地方在于,现在每件物品都有一个固定的数量,仍然是求在特定的背包容量下的最大放进背包的物品价值。解法:明白了完全背包,多重背包问题就非常简单了。它的解决方法和完全背包问题基本相同,完全背包问题需要计算在固定容量下,每件物品能拿放的数目,对于一个固定背包容量,每件物品可以拿很多个(只要不超出背包的总容量就可以),但是在多重背包,每件物品的数量是固定的,有限的。所以就是在这基础上多加个限定条件,就是:每件物品可以拿多个,但不能超过最大限制的数量,同时也不能超过背包的总容量。也就是说之前完全背包的最大数量是$+\infty$,现在将最大数量修改成对应每件物品的数量即可。递归方法:控制$F(i, V)=w_i +F(i-1, V-c_i)$的最大递归次数不能大于物品数量;动态规划方法:给while循环设置条件使得它的重复次数不能大于物品的个数。

#递归求解
def rec_multiple_bag(c, w, v, i, num, count=0):
    '''
    param num: 每件物品对应的数量
    param count: 统计的递归次数
    '''
    if i > len(c)-1:
        return 0
    elif v <= 0:
        return 0
    elif v > 0:
        if c[i] <= v and count < num[i]:
            A = w[i] + rec_multiple_bag(c, w, v-c[i], i, num, count+1) #重点在于控制递归深度(最大递归次数),这里A的最大递归次数不能大于物品的数量
            B = rec_multiple_bag(c, w, v, i+1, num, 0)
            c = max(A, B)
        elif c[i] > v or count >= num[i]:
            c = rec_multiple_bag(c, w, v, i+1, num, 0)  
    return c

#动态规划求解
def dp_multiple_bag(c, w, v, num):
    '''
    param num: 每件物品对应的数量
    '''
    res = [[0 for j in range(v+1)]for i in range(len(c)+1)]
    for i in range(1, len(c)+1):
        for j in range(1, v+1):
            if j<c[i-1]:
                res[i][j] = res[i-1][j]
            elif j>=c[i-1]:
                A = -1
                t = j
                k = 1
                while t>=c[i-1] and k<=num[i-1]:   #和递归一样,这里也是要控制最大的重复放置的次数不能大于物品的数量
                    t -= c[i-1]
                    A = max(A, k * w[i-1] + res[i-1][t])
                    k += 1 
                res[i][j] = max(A, res[i-1][j])
    return res[len(c)][v]

代码我测试了几个例子没有发现问题,如果有错误可以在评论区告诉我。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM