【算法】【动态规划篇】第1节:0-1背包问题

本期任务:介绍算法中关于动态规划思想的几个经典问题

【算法】【动态规划篇】第1节:0-1背包问题

【算法】【动态规划篇】第2节:数字矩阵问题

【算法】【动态规划篇】第3节:数字三角形问题

【算法】【动态规划篇】第4节:硬币找零问题

【算法】【动态规划篇】第5节:剪绳子问题


一、问题描述

给定n种物品和一背包。物品i的重量是wi>0,其价值为vi>0,背包的容量为c。
求在背包容量限制下物品的最大价值? 

输入:
n, c = 4, 7
w = [3, 5, 2, 1]
v = [9, 10, 7, 4]

输出:
20
[0, 2, 3]
    

二、算法思路

1. 策略选择

一个模型:

  • 0-1背包问题是典型的“多阶段决策最优解”问题,每个物品决策一次(拿或者不拿),共决策n次(n为物品数量);最优解是背包容量限制下的最大价值。

示例对应的递归树如下,其中 f ( 0 , 7 , 0 ) f(0,7,0) 代表装入编号为0的物品前,背包容量为7,当前总价值为0:
在这里插入图片描述

三个特征:

  • 重复子问题:
    • 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
    • 本题中不同的物品选择方案,可能导致相同的物品价值,如递归树中的 f ( 4 , 4 , 9 ) f(4,4,9) (仅取第一件物品)与 f ( 4 , 4 , 11 ) f(4,4,11) (仅取最后两件物品)
  • 无后效性:
    • 前面阶段的状态确定之后,不会被后面阶段的决策所改变。一般而言,满足多阶段决策最优解模型的问题都满足无后效性,特例情况,如八皇后问题解数独问题等。
  • 最优子结构:
    • 后面阶段的状态可以通过前面阶段的状态推导出。
    • 本题中,每一个状态都可以通过上一轮的状态推倒而来,如 f ( 4 , 1 , 20 ) f(4,1,20) 可以由 f ( 3 , 2 , 16 ) f(3,2,16) 通过拿最后一个物品来达到。

综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,关于0-1背包问题的回溯求解,可以参照:【算法】【回溯篇】第7节:0-1背包问题


2. 动态规划算法思路

动态规划使用的流程:自顶向下分析问题,自底向上解决问题!

  • 使用两个一维数组来保存上一轮和本轮的状态列表,元素表示当前状态下的最大价值。
  • 更新过程(状态转移思路):

    更新装入当前物品后对其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
    dp[j + w[i]] = max(dp[j + w[i]], pre[j] + v[i])

  • 状态更新过程:
初始状态:
	 pre=[-1,-1,-1,-1,-1,-1,-1,-1]
	 dp=[-1,-1,-1,-1,-1,-1,-1,-1]
	 
编号为0的物品决策完成:
	 pre=[-1,-1,-1,-1,-1,-1,-1,-1]
	 dp=[0,-1,-1,9,-1,-1,-1,-1]
	 
编号为1的物品决策完成:
	 pre=[0,-1,-1,9,-1,-1,-1,-1]
	 dp=[0,-1,-1,9,-1,10,-1,-1]
	 
编号为2的物品决策完成:
	 pre=[0,-1,-1,9,-1,10,-1,-1]
	 dp=[0,-1,7,9,-1,16,-1,-1]
	 
编号为3的物品决策完成:
	 pre=[0,-1,7,9,-1,16,-1,-1]
	 dp=[0,4,7,11,13,16,20,17]
	 
所以,最大价值为:20

三、Python代码实现

class Package01():
    def __init__(self, n, c, w, v):
        self.n = n  # 物品数量
        self.c = c  # 背包容量
        self.w = w  # 物品重量
        self.v = v  # 物品价值

    def package01(self):
        """
        动态规划思路(状态转移思路):使用两个一维数组来保存上一轮和本轮的状态列表
            更新装入当前物品后对本行其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
                dp_arr[j + self.w[i]] = max(dp_arr[j + self.w[i]], existed[j] + self.v[i])
        """
        dp_arr = [-1] * (self.c + 1)  # 记录当前条件下的最大价值

        dp_arr[0] = 0
        dp_arr[self.w[0]] = self.v[0]

        for i in range(1, self.n):
            pre = list(dp_arr)
            for j in range(self.c + 1):
                if pre[j] >= 0 and j + self.w[i] <= self.c:
                    dp_arr[j + self.w[i]] = max(dp_arr[j + self.w[i]],
                                                pre[j] + self.v[i])

        return max(dp_arr)


def main():
    n, c = 4, 7
    w = [3, 5, 2, 1]
    v = [9, 10, 7, 4]

    pk = Package01(n, c, w, v)
    print(pk.package01())


if __name__ == '__main__':
    main()

输出结果:

20

四、问题拓展

1.题目描述

给定n种物品和一背包。物品i的重量是wi>0,其价值为vi>0,背包的容量为c。
问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

2.问题分析
相较于原问题,题目还要求输出最大价值的物品清单。解决方案,使用 n ( c + 1 ) n*(c+1) 的二维数组用来存储每个状态下的最大价值,求出最大价值之后在反推物品清单即可。

3.具体代码

class Package01():
    def __init__(self, n, c, w, v):
        self.n = n  # 物品数量
        self.c = c  # 背包容量
        self.w = w  # 物品重量
        self.v = v  # 物品价值

        self.res = []  # 记录最大价值对应的物品清单
        self.max_v = 0  # 记录当前状态最大价值

    def package01(self):
        """
        动态规划思路(状态转移思路):
        针对上一轮非负位置,
            更新当前位置:取当前位置与上一行位置的较大者
                self.dp_arr[i][j] = max(self.dp_arr[i][j], self.dp_arr[i - 1][j])
            更新装入当前物品后对本行其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
                self.dp_arr[i][j + self.w[i]] = max(self.dp_arr[i][j + self.w[i]], self.dp_arr[i - 1][j] + self.v[i])
        """
        self.dp_arr = [[-1] * (self.c + 1) for _ in range(self.n)]  # 记录当前条件下的最大价值
        self.dp_arr[0][0] = 0
        self.dp_arr[0][self.w[0]] = self.v[0]
        for i in range(1, self.n):
            for j in range(self.c + 1):
                if self.dp_arr[i - 1][j] >= 0:
                    self.dp_arr[i][j] = max(self.dp_arr[i][j], self.dp_arr[i - 1][j])
                    if j + self.w[i] <= self.c:
                        self.dp_arr[i][j + self.w[i]] = max(self.dp_arr[i][j + self.w[i]],
                                                            self.dp_arr[i - 1][j] + self.v[i])

        self.max_v = max(self.dp_arr[-1])
        print(self.max_v)

    def printRes(self):
        # 反推最大价值的物品清单
        max_v = self.max_v
        for i in range(self.n - 1, -1, -1):
            if max_v - self.v[i] >= 0 and max_v - self.v[i] in self.dp_arr[i - 1]:
                self.res.append(i)
                max_v -= self.v[i]
        print(self.res)


def main():
    n, c = 4, 7
    w = [3, 5, 2, 1]
    v = [9, 10, 7, 4]

    pk = Package01(n, c, w, v)
    pk.package01()
    pk.printRes()


if __name__ == '__main__':
    main()

输出结果:

20
[3, 2, 0]
原创文章 36 获赞 32 访问量 2739

猜你喜欢

转载自blog.csdn.net/weixin_43868754/article/details/105707371