动态规划之搞懂01背包(代码随想录个人笔记)

1. 01背包理论基础

有n种物品,每种物品只有一个,每个物品有自己的重量和价值,有一个最多只能放重量为m的背包,问装满整个背包的最大价值是多少

1.1 确定dp[i][j]的含义

dp[i][j]:下标为[0,i]的物品任取,放进容量为j的背包的最大价值为dp[i][j]
i表示的是物品的下标,j表示的是背包最大容量,dp[i][j]表示的是价值
背包容量为j的情况下,考虑前i个物品的最佳组合,得到最大价值

1.2 确定递推公式
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
           不放物品i         放物品i

解释:选择某个物品的时候只有放或者不放两种选择
解法归纳
一、如果装不下当前物品,那么前n个物品的最佳组合和前n-1个物品的最佳组合是一样的
二、如果装得下当前物品。
假设1:装当前物品,再给当前物品预留相应空间的情况下,前n-1个物品最佳组合加上当前物品的价值就是总价值(此时前n-1个物品的最佳组合是容量减去第n个物品重量的情况下)
假设2:不装当前物品,那么前n个物品的最佳组合和前n-1个物品的最佳组合是一样的
取max(假设1, 假设2),为当前最佳组合的价值
关键代码:

1.3 实现代码(二维)

方式一:第一行第一列都初始化为0

m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值

dp = [] # 初始化m+1行,n+1列的背包
for i in range(m+1):
	temp = [0] * (n+1)
	dp.append(temp)

for i in range(1, m): # 第一行和第一列都初始化为0
	for j in range(1, n+1):
		if j-weight[i] < 0:
			dp[i][j] = dp[i-1][j]
		else:
			dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])

print(dp[-1][-1])

方式二:第一列初始为0,第一行根据题目条件动态初始化

m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值

dp = [] # 初始化m行,n+1列的背包
for i in range(m):
	temp = [0] * (n+1)
	dp.append(temp)

# 初始化第一行
# 含义:前0个物品,背包容量在j的最大价值,此时的j必须大于weight[0],第一个物品重量
for i in range(weight[0], n+1):
	dp[0][j] = value[0]

for i in range(1, m):
	for j in range(1, n+1):
		if j-weight[i-1] < 0: # 这里为什么是i-1呢,`因为weight是从0下标开始的,而i, j都是下标1开始所以减一`
			dp[i][j] = dp[i-1][j] # 拷贝上一层的数据
		else:
			dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]]+value[i-1])
			
print(dp[-1][-1])
1.4 理解二维dp压缩为一维dp

由于二维的递推公式为: dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
我们可以发现,dp[i][j] = max(正上方的值,左上方的值),数据都是由上一层拷贝而来,可以考虑将二维矩阵压缩成一行,每一次根据第一行来更新第二行的信息,然后再把第二行替换为第一行,为了避免覆盖的问题,遍历背包可以从右边往左边遍历。因为如果从左边往右边遍历的话,取左上角的值就会被更新,从右边向左边左上角的值保留的依然是上一层还没有更新的数据
矩阵压缩为:dp[j] = max(dp[j], dp[j-weight[i]]+value[i])

一维dp代码

m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值

dp = [0] * (n+1)
for i in range(m): # 遍历物品
	for j in range(n, weight[i]-1, -1): # 遍历背包,从大到小
		dp[j] = max(dp[j], dp[j-weight[i]]+value[i])

print(dp[-1])

2.01背包相关题目

2.1 分割等和子集 (考虑能不能装满背包)

题目描述见: 分割等和子集
思路

为什么这里的重量和价值是一样的呢?
首先:想一想01背包的目标:选物品装入背包,让背包的价值最大
因此本题的目标是:让装满背包的价值最大,但是题目描述中没有价值这个概念,如何使得价值最大呢?
不妨物品的单位重量的价值都相等,将数组中的元素视为重量同时也视为价值
因此重量最大就是价值最大

def canPartition(self, nums: List[int]) -> bool:
        if sum(nums) % 2 != 0:
            return False
        bagSize = sum(nums) // 2

        # 问题转变为nums数组中的元素,能否装进bagSize大小的背包中

        # 确定dp数组的含义;dp[i][j] 前i个物品任取,放到容量为j的背包中得到的最大价值是dp[i][j],这里的重量和价值都一样
        # 确定递推公式: dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])
        dp = []
        for i in range(len(nums)+1): # 物品
            temp = [0] * (bagSize+1) # 容量
            dp.append(temp)
        
        for i in range(1, len(nums)+1):
            for j in range(1, bagSize+1):
                if j - nums[i-1] < 0:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i-1]]+nums[i-1])

        if dp[i][j] == bagSize:
            return True
        return False
2.2 最后一块石头的重量II (考虑能装多少装多少)

题目描述见:最后一块石头的重量II
思路

分成两堆石头,并且石头的重量尽可能相等,然后相互抵消,以下展示了二维dp的做法

def lastStoneWeightII(self, stones: List[int]):
      bagSize = sum(stones) // 2  # 一个背包尽量凑成这么大、
       # dp[i][j] 含义前i个元素任取,装进容量为j的背包中得到的最大重量/价值
       dp = []
       for i in range(len(stones)+1):
           temp = [0] * (bagSize+1)
           dp.append(temp)

       for i in range(1, len(stones)+1):
           for j in range(1, bagSize+1):
               if j - stones[i-1] < 0:
                   dp[i][j] = dp[i-1][j]
               else:
                   dp[i][j] = max(dp[i-1][j], dp[i-1][j-stones[i-1]]+stones[i-1])

       # 得到的dp[-1][-1]就是左边最大的能装入的容量 右边的重量减去左边的重量
       return sum(stones) - dp[-1][-1] - dp[-1][-1]
2.3 目标和(考虑有多少种装进去的方式)

题目描述见:目标和

这里要注意第一行和第一列为什么这样初始化,以及内在得含义(难点
递推公式;放+不放,如果不妨得话容量不会减少,放的话容量就要减少

def findTargetSumWays(self, nums: List[int], target: int):
        total = sum(nums)
        if total < abs(target) or (total+target) % 2 != 0:
            return 0
        
        # 1.确定dp[i][j]含义:前i个物品装入容量为j得背包有几种方法
        # 2.确定递推公式: dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
        bagSize = (total+target) // 2
        dp = []
        for i in range(len(nums)):
            temp = [0] * (bagSize+1)
            dp.append(temp)

        # 初始化第一行和第一列
        dp[0][0] = 1 # 物品0装入背包容量为0得背包只有一种方法,就是不装入
        for j in range(1, bagSize+1):
            if j == nums[0]:
                dp[0][j] = 1

        numZero = 0 # 第一列物品0得数量
        for i in range(len(nums)):
            if nums[i] == 0:
                numZero += 1
            dp[i][0] = 2 ** numZero
        # 递推
        for i in range(1, len(nums)):
            for j in range(1, bagSize+1):
                if j - nums[i] < 0:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
        return dp[-1][-1]
2.4 一和零(装满这个背包最多有多少个物品)

题目描述见:一和零

1.本题以三维的方式展现,对于三维数组初始化的理解,可以参考三维数组初始化

def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        # 1.确定dp数组的含义:dp[i][j][k] 遍历前i个物品,装满j个0和k个1的背包,最多要多少个物品
        # 2.确定递推公式: dp[i][j][k] =  max(dp[i-1][j][k], dp[i-1][j-x][k-y]+1)
        
        dp = [] # [[[0],[0],[0]]]
        
        # 初始化,初始化len(strs)+1层
        for i in range(len(strs)+1):
            dp.append([])
            # 中间层,初始化m+1行
            for j in range(m+1):
                dp[i].append([])
                dp[i][j] = [0] * (n+1)

        for i in range(1, len(strs)+1):
            # 计算0和1的个数
            zeros = strs[i-1].count('0')
            ones = strs[i-1].count('1')
            for j in range(m+1):
                for k in range(n+1):
                    if j -zeros < 0 or k-ones < 0:
                        dp[i][j][k] = dp[i-1][j][k]
                    else:
                        dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-zeros][k-ones]+1)
        return dp[-1][-1][-1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值