一篇文章吃透背包问题---python版

一篇文章吃透背包问题---python版,第1张

一篇文章吃透背包问题—python版

原文来自一篇文章吃透背包问题因为其代码是c++写的,这里我重新整理并用python重写了一遍,供大家参考
当然,想要了解更多可以看看背包九讲。


背包问题

背包定义

给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
注意:
1、背包容量target和物品nums的类型可能是数,也可能是字符串
2、target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合
那么对应的背包问题就是下面我们要讲的背包分类

背包问题分类

常见的背包类型主要有以下几种:
1、0/1背包问题:每个元素最多选取一次
2、完全背包问题:每个元素可以重复选择
3、组合背包问题:背包中的物品要考虑顺序
4、分组背包问题:不止一个背包,需要遍历每个背包

而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合

因此把背包类型和问题类型结合起来就会出现以下细分的题目类型:
1、0/1背包最值问题
2、0/1背包存在问题
3、0/1背包组合问题
4、完全背包最值问题
5、完全背包存在问题
6、完全背包组合问题
7、分组背包最值问题
8、分组背包存在问题
9、分组背包组合问题
这九类问题我认为几乎可以涵盖力扣上所有的背包问题

背包问题解题模板

所有dp类问题离不开五大步骤:
1、确定dp矩阵维度,横坐标表示什么,纵坐标表示什么
2、初始化dp
3、推导dp状态转移方程
4、确定遍历循环顺序,先循环什么,正序逆序?后循环什么
5、确定输出内容在dp中的位置,常见是dp[-1]或者max(dp)

如果觉得dp转移方程很绕的话,你应该在dp矩阵输出之前手推出dp矩阵,如此反复多练习几次基本就不会再范迷糊了。


'''
背包问题
基础二维背包
'''
def bags():
	weight= [1,3,4]
	value = [15,20,30]
	bagweight = 4
	#1, dp设置为二维矩阵,横坐标表示选择0-i的物品,纵坐标表示背包容量为j
	dp = [[0 for _ in range(bagweight + 1)] for _ in range(len(weight))]
	#2, dp初始化
	for j in range(bagweight+1):
		if j>=weight[0]:
			dp[0][j] = value[0]
	#3,4, 确定状态转移方程以及循环次序
	for i in range(1,len(weight)):
		for j in range(bagweight+1):
			if j>= weight[i]:
				dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]] + value[i])
			else:
				dp[i][j] = dp[i-1][j]
	print(dp)
	return dp[-1][-1]
print(bags())

可以看到上面的dp矩阵中,转移方程没有用到dp[i]相关的数据,因此对于dp[i]来说是冗余的,我们可以将其转化为一个一维dp

'''
二维背包简化为一维
'''
def bags():
	weight= [1,3,4]
	value = [15,20,30]
	bagweight = 4
	#1, dp设置为一维矩阵 dp[i]表示容量为i的背包能装下的最大价值
	dp = [0 for _ in range(bagweight + 1)] 
	#2, dp初始化
	for j in range(bagweight+1):
		if j>=weight[0]:
			dp[j] = value[0]
	#3,4, 确定状态转移方程以及循环次序
	for i in range(1,len(weight)):
		for j in range(bagweight+1):
			if j>= weight[i]:
				dp[j] = max(dp[j],dp[j-weight[i]] + value[i])
			else:
				dp[j] = dp[j]
	print(dp)
	return dp[-1]
print(bags())

为了和之前二维的统一形式,这里就不做简化了,方便大家对比。


可以看到,对于一个通用的dp问题来说,难点主要在转移方程的迭代上,如果你不想动脑子的话,可以尝试理解一下下面这类问题的模板。


分类解题模板

背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程,
根据背包的分类我们确定物品和容量遍历的先后顺序,根据问题的分类我们确定状态转移方程的写法
首先是背包分类的模板:
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包(考虑顺序):外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

这里内循环和外循环总结的不太完美,除非是排序问题,否则没有内循环和外循环的分别
对于排序问题来说,只能外循环是 nums, 内循环是 sum,这样子才能保证顺序 比如说 nums是 1和 5,sum 是 6, 如果外循环是 nums,内循环是 sum, 保证了当 sum = 6 的时候 只能是 6=1+5 存在顺序是,先1 后 5 如果 外循环是 sum, 内循环是 nums, 当sum = 6 的时候 有 6 = 1+5 和 6 = 5+1 两种
具体例子 有5种硬币,面值分别为1,5,10,20,25。


给你一个总额s,问有多少种找零方式?
注意比如总额为6,则[1,5],[5,1]显然是同一种找零方式,这两种只能算作一种 所以只能采用 外循环是 nums(1,5) ,内循环是sum(6)

然后是问题分类的模板:
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num];

这样遇到问题将两个模板往上一套大部分问题就可以迎刃而解
下面看一下具体的题目分析:

  1. 零钱兑换
    零钱兑换:给定amount,求用任意数量不同面值的零钱换到amount所用的最少数量
    完全背包最值问题:外循环coins,内循环amount正序,应用状态方程1
'''
322. 零钱兑换
'''
coins = [1,2,5]
amount = 11
dp = [float('inf')] * (amount + 1)
dp[0]= 0
for i in range(len(coins)):
	for j in range(amount+1):
		if j >= coins[i]:
			dp[j] = min(dp[j],dp[j-coins[i]]+1)
print(dp)
  1. 分割等和子集
    分割等和子集:判断是否能将一个数组分割为两个子集,其和相等
    0-1背包存在性问题:是否存在一个子集,其和为target=sum/2,外循环nums,内循环target倒序,应用状态方程2
def canPartition(self, nums: List[int]) -> bool:
    if sum(nums)%2 !=0: return False
    target = sum(nums)//2
    dp = [[False for _ in range(target+1)] for _ in range(len(nums))]
    for i in range(len(nums)):
        dp[i][0] = True

    for i in range(len(nums)):
        for j in range(target+1):
            if j >= nums[i]:
                dp[i][j] = dp[i-1][j-nums[i]] or dp[i-1][j]
            else:
                dp[i][j] = dp[i-1][j]
    return dp[-1][-1]
  1. 目标和
    目标和:给数组里的每个数字添加正负号得到target
    数组和sum,目标和s, 正数和x,负数和y,则x+y=sum,x-y=s,那么x=(s+sum)/2=target
    0-1背包不考虑元素顺序的组合问题:选nums里的数得到target的种数,外循环nums,内循环target倒序,应用状态方程3

这里值得注意,因为该题中的组合问题有正有负,如果使用一维dp进行求解时需注意遍历的方向,内层的target应该逆序遍历防止需要使用的数组被生成结果覆盖

如果正常建dp表的话,由于有正负数的存在,我们的列数可能为2*target-1,这样处理起来会比较麻烦,因此,可以将其转化为一个背包问题: 正数和x,负数和y,则x+y=sum,x-y=s,那么x=(s+sum)/2=target,同样的,我们也可以将其转化为一维dp

neg = (sum(nums)-target)//2
if (sum(nums)-target)%2 != 0: return 0
dp = [0]*(abs(neg) + 1)
dp[0] = 1
for i in range(len(nums)):
    for j in range(neg,-1,-1):
        if j >= nums[i]:
                dp[j] += dp[j-nums[i]]
print(dp)
  1. 完全平方数
    完全平方数:对于一个正整数n,找出若干个完全平方数使其和为n,返回完全平方数最少数量
    完全背包的最值问题:完全平方数最小为1,最大为sqrt(n),故题目转换为在nums=[1,2…sqrt(n)]中选任意数平方和为target=n
    外循环nums,内循环target正序,应用转移方程1
dp = [0]*(n+1)
dp[0] = 0
for i in range(1,n+1):
    min_num = float('inf')
    for j in range(1,int(math.sqrt(i))+1):
        min_num = min(min_num, dp[i - j**2])
    dp[i] = min_num+1
return dp[-1]
  1. 组合总和 Ⅳ
    组合总和IV:在nums中任选一些数,和为target
    考虑顺序的组合问题:外循环target,内循环nums,应用状态方程3

做累了,持续更新中。








欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/568838.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-09
下一篇 2022-04-09

发表评论

登录后才能评论

评论列表(0条)

保存