1.参考资料
背包问题从简单到复杂有:
0-1背包:每种物品只能放入一次
完全背包:每种物品放入次数不限
多维费用背包:物品的费用可能有重量和体积等多个方面
复杂约束背包:背包的物品相互约束或依赖
推荐的材料是背包问题九讲,初步学习了0-1背包和完全背包问题。
2. 0-1背包问题
所有的背包问题都可以由0-1背包演化而来。
2.1 问题定义:
有一些物品,这些物品的重量为[cost i],对应的价值为[value i],将它们装进容量为V的背包,求能获得的最大价值。
2.2 状态定义:
F[i,s]表示将前i件物品装进容量为s的背包时能获得的最大价值,则F[I,V]就是最优解。
2.3 状态转移方程:
F[i,s] = max{ F[i-1,s](不把第i件物品放入背包),F[i-1,s-cost i]+value i(把第i 件物品放入背包)}
2.4 求解算法:
2.5 优化:
上述F数组是I*V,可以优化成一维数组。只要把重量的遍历顺序固定为V---cost i。因为每次更新时要用到前面的值,若从小到大更新,那么后边更新用到的前面的值是更新后的F[i,s-cost i]而非F[i-1, s-cost i]。实际上,求解时通常优先考虑一维数组。
算法改为:
2.6 初始化:
若把题目的要求改为“装满背包时,最大价值多少”,则初始化F[0]=0,F[1..V]为-,因为s=0,问题可能有解,而没东西可放但是容量不为0,说明没有可行解,用无穷来表示这种状态。
其他的背包问题也要注意初始化问题,根据情况选为负无穷或正无穷。
3. 完全背包问题
3.1 定义
与0-1背包唯一的不同是每种物品数量不限。
3.2 状态转移方程
F[i,s] = max{ F[i-1,s](根本不把第i件物品放入背包),F[i,s-cost i]+value i(把第i 件物品放入背包一次或多次)}唯一的区别在于第二项从i-1到i。
3.3 求解算法
与0-1背包唯一的不同是重量的遍历顺序相反,完全背包的每一项更新要用到前边的更新后的值,所以要从小到大遍历,这样计算F[i,s]时,保证s-cost i的位置上是更新后的F[i, s-cost i]而非F[i-1, s-cost i]。
解背包问题的注意点
1.初始化
2.遍历顺序
3.数组的下标不要越界,比如 cost i可能大于V
4. leetcode
题目定义
非负数组分为两个子数组,两个子数组的和相等。给定一个数组,请问是否能分开。
代码
class Solution(object):
def canPartition(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
if not nums:
return True
if len(nums)==1 :
return True if nums[0]==0 else False
nums_sum = 0
for i in nums:
nums_sum += i
if nums_sum%2==1:
return False
v = nums_sum/2
dp = [-1]*(v+1)#因为要装满,所以初始化为-1表示无解
dp[0] = 1#初始化为1表示有解
for i in nums:
for idx in range(i,v+1)[::-1]:
if dp[idx]==1 or dp[idx-i]==1:
dp[idx]=1
return True if dp[-1]==1 else False
因为是要装满且是看问题有无解,所以初始化为-1表示无解,初始化为1表示有解。
问题定义
给一组人民币取值数组,给定一个目标值,求能用人民币数组表示的方案的最小张数,无解返回-1.
代码
class Solution(object):
def coinChange(self, coins, amount):
if not coins or amount<0:
return -1
dp = [10000]*(amount+1)
dp[0] = 0
for i in coins:
for idx in range(i,amount+1):
dp[idx]=min(dp[idx],dp[idx-i]+1)
return dp[-1] if dp[-1]<10000 else -1
因为要求最小解,所以无解的初始值为正无穷。这是一个通用技巧。
问题定义
给定0和1的个数,从一个字符串数组中选出不超过0,1的个数的最大字符串数。
代码
class Solution(object):
def findMaxForm(self, strs, m, n):
if not strs:
return 0
dp = [[0 for i in range(n+1)] for j in range(m+1)]
for s in strs:
c0 = s.count('0')
c1 = s.count('1')
if c0>m or c1>n:
continue
for x in range(c0,m+1)[::-1]:
for y in range(c1,n+1)[::-1]:
dp[x][y] = max(dp[x][y],dp[x-c0][y-c1]+1)
return dp[m][n]
与0-1背包的不同是F成了二维数组,分别表示两个约束的取值。也多了一层循环。
python新建二维list使用列表产生式,此处容易踩坑!!!而且行数列数顺序先后要注意!!!
问题定义
给出一个数组和目标值,求 和为目标值(正数)的组合数。注意,顺序不一样算两种解。
代码
from collections import deque
class Solution(object):
def combinationSum4(self, nums, target):
if not nums:
return 0
dp = [0]*(target+1)
dp[0]=1
for i in range(1,target+1):
for num in nums:
if i-num>=0:
dp[i]+=dp[i-num]
return dp[-1]
问题定义
给定数组,每个元素可以取为正或负,问有几种方法使数组所有元素和为目标值。
代码
class Solution(object):
def findTargetSumWays(self, nums, S):
if not nums:
return 0
sum_nums = sum(nums)
if S>sum_nums or (sum_nums-S)%2==1:
return 0
target = (sum_nums-S)/2
dp = [0]*(target+1)
dp[0] = 1
for num in nums:
for idx in range(num,target+1)[::-1]:
dp[idx] = dp[idx]+dp[idx-num]
return dp[-1]
数组全取正数,和为sum_nums,与target差值的一半,就是取值为负的数的和。
问题变成了,选取数组的若干个数,使和为目标值(sum_nums-target)/2。
即0-1背包问题 装满。