背包问题之:01背包、完全背包、多重背包(本文源码可求物品放置列表)

主要理解得益于:https://blog.csdn.net/a784586/article/details/63262080
其通俗易懂的讲解着实厉害,部分内容也来自与这篇博文



动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中, 可能会有很多可行解,每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划可以将一个复杂问题分解问若干子问题,然后求解子问题,从而得到原始问题的解。
这本身是一种分治法的思想,但是动态规划分解后的子问题往往不是独立的,即下一个阶段的求解必须是建立在上一个子阶段的基础上的。可以简单理解为一个爬楼梯的过程,你要爬到第二格楼梯,一定要先爬第一格。


背包问题

n 种重量和价值分别为 w e i g h t i v a l u e i 的物品,假设有一个容量为 w 的背包,求怎么样装这个背包,才能使得 1 n k i w e i g h t i k i 0 不超过背包的总重量,并获得最大的价值 1 n k i v a l u e i
这个问题确实很难,因为从物品的组合方式多种多样,我怎么知道该如何选才能达到最优?下面以一个简单的例子来说明动态规划在这里应该怎么用。
这里写图片描述
如上图,现在我们有一个能容纳重量为 w 的背包,和5种物品 a b c d e ,直观上确实很难理解怎么直接放置才能实现价值最大化。

1、假设这里每种物品至多只能取一次,并且我们先不管这个背包真实容量11,我们假设这个背包容量其实只有1,并且此时此刻可选物品也只有a,显然,在这种情况下,a能放入这个背包,并得到了价值1。这时候假设我们可选物品多了一件b,容量仍为1,但b的重量大于1,放不下,与此类似,我们可知,即使可选物品变成5件,因为其他物品的重量都超过了当前这个背包容量1,我们可放的物品仍然只有a,总的价值还是为1。

2、这时候我们假设背包容量变成了2,一开始还是只能放a,得到的价值为1。这时候,假设b也可选了,即此时ab均可选,那么因为b的重量小于等于2,此时价值最大的方式就是把刚刚放的a拿出来,放入b,就可以得到价值6。当 c d e 也可选,因为容量超限,显然不能再有更好的放置方案。

3、接着,我们假设背包容量变成了3,起初只有a一种物品,那么能放入,得到的价值为1,这时候再来一件b,a+b仍然不超过容量3,因此可以得到价值7

我发现这个例子要举出点鲜明的变化至少要举到重量变成5的时候,但那时候文字太多了。我就文字补一波,希望看到这里的人能脑补出来。我们假设在重量为5的情况下,一件件看物品 a b c d e 物品,开始只有a,那么放得下,得到价值1。只有ab,也放得下,得到价值7。但出现c的时候,问题来了,因为c一个的价值就超过了ab,而且重量不超限,最好的方法就是把ab都干掉,只放c。当放了c,后来再看de,因为两个都超限,最终也只放c获得了最大价值18

上面的内容非常废话,但却透露了一个思想,甚至隐含了一个公式。
我们假设 o p t ( i , w ) 为前 i 种物品在重量 w 条件下任意组合所能达到的最大化价值(好好理解一下这句话理解了背包问题基本没问题),则有

o p t ( i , w ) = m a x ( i i )

有人说这不是废话么,请看回前面的第三点,假设在c刚出现的时候,我们要怎么才能知道如何实现价值最大化,那么就是比比a+b和单个c的价值,看看谁能最大化这个利益。

这里用区区几个小例子很难表达完整,我们拓展一下物品种类,假设现在有了50种物品 o 50 ,背包容量也拓展到了110。那你怎么知道当50种物品都可选即(=50),且背包容量为110的最优方案是什么。参考前面那一段落,我们现在假设已经知道当容量<=110,且物品<50(即最多49种)的最佳放置方案。且假设第50种物品的价值为 v a l u e 50 重量为 w e i g h t 50 ,那么求解这个的办法即为,对比一下没出现 o 50 时候的最大利益 o p t ( 49 , w ) 和把 o 50 也放进去的利益 o p t ( 49 , w w e i g h t 50 ) + v a l u e 50

这里很多人会晕,其实很简单, w w e i g h t 50 也就是这个背包有重达 w e i g h t 50 的空间给这个新成员占用了,并且通过这个成员增多了价值 v a l u e 50 。但你其余的49种只剩下 w w e i g h t 50 这么多空间进行分配了,所以也要让49种任意组合得到最大化价值。这个公式一般化,有:

o p t ( i , w ) = m a x { o p t ( i 1 , w ) , o p t ( i 1 , w w e i g h t i ) + v a l u e i }

这个公式很多人看起来就晕圈了,其实只要把i从1开始(手算都可以),慢慢推导过去,你就会发现,我去,那么简单?


问题具体化

这里再提一下上面那个例子
这里写图片描述
求解这个问题,其实画个表格,慢慢算即可,如下:
这里写图片描述
里面几个主要的数字我都标红了,前面都有提到,这个表只要慢慢算到右下角就可以了,最右下角就是当w为11,n为5时候的最优值。我用python跑出的完整表格如下:
这里写图片描述


01背包问题

其实0101,挺形象的,0就是不放,1就是至多放一个。也就是物品只有放或者不放进背包,要么就不放,要么就只放一个。前面提到的那个案例就是01背包问题,01背包问题是所有背包问题的基础,基本上理解了01问题,接下来的完全背包问题,多重背包问题都能很快明白。
像我前面提到了大堆文字,其实数学公式非常简洁明了,01问题的最优价值假设为 o p t ( i , w ) ,那么具体迭代或者说,状态转移方程(你可以理解为这个阶段和上一个阶段之间缠绵的关系)如下:
( 1 ) . o p t ( i , w ) = 0 i f i 0
( 2 ) . o p t ( i , w ) = o p t ( i 1 , w ) i f w e i g h t i w
( 3 ) . o p t ( i , w ) = m a x { o p t ( i 1 , w ) , o p t ( i 1 , w w e i g h t i ) + v a l u e i } o t h e r w i s e
直接上代码:

def main():
    w = 11  # 背包容量
    n = 5  # 物品种类/个数
    # 对应为物品a,b,c,d,e
    value = [1, 6, 18, 22, 28]  # 价值
    weight = [1, 2, 5, 6, 7]  # 开销

    # 01问题
    zero_one_problem(w, n, weight, value)


# 01问题
def zero_one_problem(w, n, weight, value):
    # 创建一个大小为w+1 * n+1 的全零矩阵,用来放结果
    matix = [[0 for i in range(w + 1)] for j in range(n + 1)]

    # 背包种类个数开始上升
    for row in range(1, n + 1):
        # 容量从1开始增长
        for col in range(1, w + 1):
            # 先看看新来的花费是不是大于当前容量
            if (weight[row - 1] > col):
                # 如果是,就在前面找最大值
                matix[row][col] = matix[row - 1][col]
            else:
                # 如果不是,就看看新增一个物品价值牛逼点,还是不用这个物品也很厉害
                matix[row][col] = max(matix[row - 1][col], value[row - 1] + matix[row - 1][col - weight[row - 1]])
    print('0/1背包问题的各种条件下的最优值矩阵:')
    for it in matix:
        for i in it:
            print(i, '\t', end='')
        print()
    # 看看对应某一个位置对应放了什么物品
    res = [0 for heh in range(n)]
    row = n  # row和col为你要查的某个元素的坐标对应的物品放置列表
    col = w  # 可改
    # 打印矩阵
    while row - 1:
        if matix[row][col] == (
                matix[row - 1][col - weight[row - 1]] + value[row - 1]):
            res[row - 1] = 1
            col = col - weight[row - 1]
        row -= 1
    res[0] = int(matix[1][col] / value[0])
    print('0/1背包问题坐标为%d,%d的背包物品存放列表:' % (n, w))
    print(res)
    print('-' * 50)


if __name__ == '__main__':
    main()

这里写图片描述


完全背包问题

完全背包问题看上去高大上了很多,其实它只是,假设每一种物品的个数都变成了无穷个。也就是,只要你想放这个物品,那么想要多少有多少。虽然看上去难了很多了,其实只要把01问题的第二个方程去掉,然后最后一个方程 m a x 的第二个参数改成,让新来的物品个数k从0到 k m a x ,之所以会有 k m a x 这项,是因为即使物品是取之不尽的,背包容量总是有限的, k m a x 的得来就是 k w e i g h t i 向下取整得到(即假设新来的物品尽可能放),公式变成:
( 1 ) . o p t ( i , w ) = 0 i f i 0
( 2 ) . o p t ( i , w ) = m a x { o p t ( i 1 , w ) , o p t ( i 1 , w k × w e i g h t i ) + k × v a l u e i } o t h e r w i s e
其中 0 k k m a x ,因为要让新来的物品从0到最多都试一试,看看取几个能组合出最厉害的组合。

def main():
    w = 11  # 背包容量
    n = 5  # 物品种类/个数
    # 对应为物品a,b,c,d,e
    value = [1, 6, 18, 22, 28]  # 物品价值
    weight = [1, 2, 5, 6, 7]  # 物品重量

    # 完全背包问题
    complete_problem(w, n, weight, value)


# 完全背包问题
def complete_problem(w, n, weight, value):
    # 创建一个大小为w+1 * n+1 的全零矩阵,用来放结果
    matix = [[0 for i in range(w + 1)] for j in range(n + 1)]

    # 背包种类个数开始上升
    for row in range(1, n + 1):
        # 容量从1开始增长
        for col in range(1, w + 1):
            max_k = int(col / weight[row - 1])  # 不限个数,所以只要新增背包上限不超过总容量即可
            for k in range(max_k + 1):
                matix[row][col] = max(matix[row - 1][col],
                                      k * value[row - 1] + matix[row - 1][col - k * weight[row - 1]])
    print('完全背包问题的各种条件下的最优值矩阵:')
    for it in matix:
        for i in it:
            print(i, '\t', end='')
        print()

    # 获取最优值矩阵中某个位置为行row,列col的点的物品放置列表
    res = [0 for heh in range(n)]
    row = n  # row和col为你要查的某个元素的坐标对应的物品放置列表
    col = w  # 可改
    # 打印矩阵
    # 这个求解方法完全是反向思维,即上面求解倒着来,要多加理解
    while row - 1:
        count = int(col / weight[row - 1])
        for k in range(count, 0, -1):
            if matix[row][col] == (
                    matix[row - 1][col - weight[row - 1] * k] + k * value[row - 1]):
                res[row - 1] = k
                col = col - k * weight[row - 1]
                break
        row -= 1
    res[0] = int(matix[1][col] / value[0])
    print('完全背包问题坐标为%d,%d的背包物品存放列表:' % (n, w))
    print(res)
    print('-' * 50)


if __name__ == '__main__':
    main()

这里写图片描述


多重背包问题

其实这个只是比完全背包问题的弱化版本,即可能物品能取的个数是有限制的。比如 a b c d e 每种均只能取3个,那么和完全背包问题的优化公式基本一样,只是 k m a x 变成了 m i n { 使 w }
( 1 ) . o p t ( i , w ) = 0 i f i 0
( 2 ) . o p t ( i , w ) = m a x { o p t ( i 1 , w ) , o p t ( i 1 , w k × w e i g h t i ) + k × v a l u e i } o t h e r w i s e
其中 0 k k m a x
代码:

def main():
    w = 11  # 背包容量
    n = 5  # 物品种类/个数
    # 对应为物品a,b,c,d,e
    value = [1, 6, 18, 22, 28]  # 物品价值
    weight = [1, 2, 5, 6, 7]  # 物品重量

    # 多重背包问题,多了一个数量限制
    num = [1, 1, 1, 1, 1]  # 背包个数,如果都是1,那么多重背包问题其实就是一个01背包问题
    # num = [3, 3, 3, 3, 3]  # 背包个数,上下两个切换注释看一看
    multiple_problem(w, n, weight, value, num)


# 多重背包问题
def multiple_problem(w, n, weight, value, num):
    # 创建一个大小为w+1 * n+1 的全零矩阵,用来放结果
    matix = [[0 for i in range(w + 1)] for j in range(n + 1)]

    # 背包种类个数开始上升
    for row in range(1, n + 1):
        # 容量从1开始增长
        for col in range(1, w + 1):
            # 有限个数,所以要在不超出背包容量和限制个数的条件下选一个值
            # 最小值满足木桶的短板理论,即能放多少,取决于少的那个数
            max_k = min(int(col / weight[row - 1]), num[row - 1])
            for k in range(max_k + 1):
                matix[row][col] = max(matix[row - 1][col],
                                      k * value[row - 1] + matix[row - 1][col - k * weight[row - 1]])
    print('多重背包问题的各种条件下的最优值矩阵:')
    for it in matix:
        for i in it:
            print(i, '\t', end='')
        print()

    # 获取最优值矩阵中某个位置为行row,列col的点的物品放置列表
    res = [0 for heh in range(n)]
    row = n  # row和col为你要查的某个元素的坐标对应的物品放置列表
    col = w  # 可改
    # 打印矩阵
    while row - 1:
        count = min(int(col / weight[row - 1]), num[row - 1])
        for k in range(count, 0, -1):
            if matix[row][col] == (
                    matix[row - 1][col - weight[row - 1] * k] + k * value[row - 1]):
                res[row - 1] = k
                col = col - k * weight[row - 1]
                break
        row -= 1
    res[0] = int(matix[1][col] / value[0])
    print('多重背包问题坐标为%d,%d的背包物品存放列表:' % (n, w))
    print(res)


if __name__ == '__main__':
    main()

这里写图片描述


后话

其实我这些代码段都是写在一起的,只不过为了布局就把它们全部拆开了。另外动态规划解决这个问题我感觉太”贪心了”,因为如果是一个多重背包问题,且背包数量有很多个,那么第一个装得太满就导致后面的背包只能装一些歪瓜裂枣。解决办法有用模拟退火算法或者遗传算法。

虽然这次背包算法也还是没有给我带来什么惊喜,但总归是有所收获的。

猜你喜欢

转载自blog.csdn.net/hiudawn/article/details/80138427