背包问题
来源:互联网 发布:mac gradle 路径 编辑:程序博客网 时间:2024/05/29 18:16
背包问题
- 背包问题
- 一. 01背包
- 1. 基础问法
- 2 求最大价值时的物品选择方案
- 3 空间优化
- 4 要求刚好装满
- 5 只考虑物品空间,忽略价值
- 5.1 求能获取的最大体积
- 5.2 求刚好装满背包的方案数
- 5.3 求刚好装满背包的最少/多选择物品数
- 二. 完全背包
- 1. 基本解法
- 2. 空间优化
- 三. 多重背包
- 1. 二进制拆分
- 参考文档
本文主要讨论01背包/完全背包/多重背包,及在此之上一些常见的变种问题,更复杂的不做讨论。
一. 01背包
1. 基础问法
01背包是所有类型背包问题的基础,它又有各式各样的问法和约束,这些变化同样适用于其他类型的背包问题。
有n个物品,价值v = [v1,v2,v3…],体积c = [c1,c2,c3…],放入总容量为
totalCapacity
的背包中,求能获得的最大价值是多少?
背包问题是动态规划
的经典问题。动态规划的基础是递归,和分治一样,都是假设子问题已经解决,由子问题的解组合计算得到父问题的解,类似数列中的递推式如f(n) = f(n-1) + f(n-2)。但在递归的过程中会出现重复计算子问题的现象,为了避免重复计算,用一个表格记录子问题的结果供查找,从下往上进行递推。
找递推式(or 状态转移方程)的思路一般是由最终状态往前回溯,考察解答最终问题需要哪些子问题。
背包问题应用动态规划:
- 子问题
根据询问确定子问题,假设f[i][j] = 前i件物品中选取若干件放入空间为j的背包中所能得到的最大价值。 - 递推式:
- 对第i件物品,不选择时,最大价值 = 前i-1件物品获得的最大价值 = f[i-1][j];
- 选择时,最大价值 = 前i-1件物品放入j-c[i]获得的最大价值 + i的价值 = f[i-1][j-c[i]] + v[i];
- 因此,
f[i][j] = max(f[i-1][j],f[i-1][j-c[i]] + v[i])
; - 当然,若c[i] > 背包总空间j,物品i只能不选,此时和第一种情况一样。
- 基础子问题的解
f[][0] = 背包空间为0时的最大价值(无法放物品) = 0,f[0][] = 没有物品可放时的最大价值 = 0(物品编号从1开始,0表示不放物品)。
注意,递推过程的是从第二行第二列开始的。
举个例子,当 c=[3,4,5], v=[4,5,6],totalCapacity=10 时,推导过程如下,注意,顺序是从上到下,从左到右一行一行(自底向上)地进行:
0
000000000034 10
004444444445 20
004555999956 30
0045669101111代码:
#!/usr/bin/python# coding=utf-8n = 3 # n个物品c = [None,3,4,5] # capacity -- 注意,物品编号从1开始v = [None,4,5,6] # valuetotalC = 10 # total capacitydef zeroOne(): t = [[None] * (totalC + 1) for i in range(n+1) ] # [n+1][totalCapacity+1] # 初始化 t[0] = [0] * (totalC + 1) # 第一行为0 for i in range(n+1): # 第一列为0 t[i][0] = 0 for i in range(1,n+1): # 从第二行第二列开始扫描. i: 当前考察的物品, j: 总空间 for j in range(1,totalC+1): if c[i] > j: # 无法放入第i个物品 -- 必定不选 t[i][j] = t[i-1][j] else: # 可以放入第i个物品 -- 可能选,可能不选 t[i][j] = max(t[i-1][j], t[i-1][j-c[i]] + v[i]) return tif __name__ == "__main__": print zeroOne()[n][totalC]
时间和空间复杂度为O(n*totalCapacity)。
2 求最大价值时的物品选择方案
求最大价值下的物品选择方案有两种办法:
1..从后面往前,根据递推式依次看第i..1个物品有没有被选择:
t = zeroOne() # print path 方法1 i = n j = totalC while i > 0 and j > 0: if t[i][j] != t[i-1][j]: # 选了第i个物品 print "第%s个物品,空间:%s,价值:%s" % (i,c[i],v[i]) j -= c[i] # 考察前一个物品 i -= 1
2..在求最大价值的同时用一个n*totalCapacity大小的二维数组path记录每个物品是否选择:
def zeroOneWithPath(): t = [[None] * (totalC + 1) for i in range(n+1) ] # [n+1][totalCapacity+1] path = [[0] * (totalC + 1) for i in range(n+1) ] # path, 0没选,1选了 # 初始化 t[0] = [0] * (totalC + 1) # 第一行为0 for i in range(n+1): # 第一列为0 t[i][0] = 0 for i in range(1,n+1): # 从第二行第二列开始扫描. i: 当前考察的物品, j: 总空间 for j in range(1,totalC+1): if c[i] > j: # 无法放入第i个物品 -- 必定不选 t[i][j] = t[i-1][j] else: # 可以放入第i个物品 -- 可能不选,可能选 t[i][j] = max(t[i-1][j], t[i-1][j-c[i]] + v[i]) # 如果选择了第i个物品,记录path if t[i][j] == t[i-1][j-c[i]] + v[i]: # <----- 只有这一个改动 path[i][j] = 1 return t,path
求物品选择方案的方法和前一种办法是一模一样的,只不过现在是根据path中的标记位,而不是最大价值与递推式来判断是否选择了该物品:
# print path 方法2 t,path = zeroOneWithPath() i = n j = totalC while i > 0 and j > 0: if path[i][j] == 1: # 选了第i个物品 # <----- 只有这一个改动 print "第%s个物品,空间:%s,价值:%s" % (i,c[i],v[i]) j -= c[i] i -= 1
实际上,两种方法的思路是一样的,不过方法2消耗的空间更多,但下面要讲的O(n)空间复杂度的算法只能使用方法2。
3 空间优化
接下来讨论怎样将算法的空间复杂度降低到O(totalCapacity)。实际上,这里用到的思路就是所谓的 滚动数组:
很多情况下,一个问题的解仅由其前面有限个子问题决定,除了这些子问题,之前记录的规模更小的子问题的解都可以舍去,所以,可以用更少的空间只保存这些子问题的解并循环利用,从而避免保存所有子问题的解。
举个例子,斐波那契数列问题中,f(n)=f(n-1)+f(n-2)
,当n为5时,f(5)仅与f(4)和f(3)有关,f(0)..f(2)都没有用了。因此,我们可以只用两个变量不停地维护f(n-1)和f(n-2),而不是将f(0)..f(n-1)的结果都保存起来。
回到01背包问题,求某个格子时,其值是由上一行的某两个格子(正上方的格子和左侧某一个格子)决定的,与更早的行没有关系,这些行的空间都被浪费了:
所以,我们可以只用一行的空间保存前一行的值并循环利用,而不需要一个表格。但此时 递推顺序不能是从左到右了,而是应该反过来,因为如果是正序,在递推的时候用的就是第i行的新值,而非前一行的数据了。实际上,用表格时也可以从右到左递推。
代码:
def zeroOne2(): t = [0] * (totalC + 1) for i in range(1,n+1): # for i in 1..n, i是物品编号 for j in reversed(range(c[i],totalC+1)): # for j in totalC..c[i], j是背包总空间 //j<物品体积c[i]时,i必定不选,c[i][j]=c[i-1][j],不用考察 t[j] = max(t[j],t[j-c[i]] + v[i]) return tif __name__ == "__main__": print zeroOne2()[totalC]
时间复杂度仍为O(n*totalCapacity),空间复杂度降低到了O(totalCapacity)。此时如果想要求最优价值的物品选择方案,只能用额外的path表格记录。
此类优化方式大部分的背包问题都可应用,后续不再赘述。
4 要求刚好装满
背包问题常见的一个限制是需要背包刚好装满,在该约束下需要注意两点:
- 在使用递推式推导的过程中必须考虑 子问题是否有解 ;
- 基础子问题的含义/解
当求最大价值时,规定当无解时用NULL表示,其他值表示刚好能装满的最大价值:- 第一列 f[][0] = 背包空间为0,认为永远是满的,且无法放物品 = 0
- 第一行(除第一个) f[0][1..totalCapacity] = 没有物品可放,此时除了空间为0的背包,其他背包永远不可能放满 = NULL
- 其他变种问题类似。
以上两点适用于所有需要装满背包的场景。
求最大价值的推导过程如下:
0
NullNullNullNullNullNullNullNullNullNull34 10
nullnull4nullnullnullnullnullnullnull45 20
nullnull45nullnull9nullnullnull56 30
nullnull456null91011null代码:
def zeroOneFullPack(): t = [[None] * (totalC + 1) for i in range(n+1) ] # [n+1][totalCapacity+1] # 初始化 t[0] = [None] * (totalC + 1) # t[0][1..totalCapacity] = None for i in range(n+1): # t[][0] = 0 t[i][0] = 0 for i in range(1,n+1): for j in range(1,totalC+1): if c[i] > j: # 无法放入第i个物品 -- 必定不选 t[i][j] = t[i-1][j] else: # 可以放入第i个物品 a = t[i-1][j-c[i]] # a: 前i-1个物品放入j-c[i]空间 b = t[i-1][j] # b: 前i-1个物品放入j空间 if a != None and b != None: # a和b都有解,则用递推式 t[i][j] = max(b,a + v[i]) elif a == None and b == None: # a和b都无解,则无解 t[i][j] = None elif a == None: # a无解而b有解,此时一定不能选择物品i,选择则无解 t[i][j] = b elif b == None: # a有解而b无解,此时只能选择i,不选则无解 t[i][j] = a + v[i] return tif __name__ == "__main__": print zeroOneFullPack()[n][totalC]
5 只考虑物品空间,忽略价值
有时题目只关心物品的体积,而不涉及物品的价值,下面提到的几类问题都是如此。
5.1 求能获取的最大体积
比较常见,简化的01背包问题,每个物品的价值即为其体积,代码略。
例子:
- 给一个整数的集合,要把它分成两个集合,要两个集合的数的和最接近
5.2 求刚好装满背包的方案数
类似的:
- 子问题
f[i][j]为前i件物品放入j空间,刚好装满的方案数,无解时为0 - 递推式
f[i][j] = f[i-1][j] (不选i) + f[i-1][j-c[i]] (选i)
如果不选i,”前i件物品装满j空间的方案数” 等于 “前i-1件物品装满j空间的方案数” ;选i,则等于”前i-1件物品装满j-c[i]空间的方案数”。二者相加即为f[i][j]。- c[i] > j时一定不能选i,f[i][j] = f[i-1][j]
- 基础子问题的解
和 求装满条件下最大价值 的场景一样,- 第一列 f[][0] = 背包空间为0,认为永远是满的,只有一种装满的方案(不放物品) = 1
- 第一行(除第一个) f[0][1..totalCapacity] = 没有物品可放,此时除了空间为0的背包,其他背包永远不可能放满 = 0
def waysToFillPack(): t = [[None] * (totalC + 1) for i in range(n+1) ] # [n+1][totalCapacity+1] # 初始化 t[0] = [None] * (totalC + 1) # t[0][1..totalCapacity] = None for i in range(n+1): # t[][0] = 1 t[i][0] = 1 for i in range(1,n+1): for j in range(1,totalC+1): if c[i] > j: # 无法放入第i个物品 -- 必定不选 t[i][j] = t[i-1][j] else: a = t[i-1][j-c[i]] # a: 前i-1个物品放入j-c[i]空间 b = t[i-1][j] # b: 前i-1个物品放入j空间 if a != None and b != None: # a和b都有解,则用递推式 <---- 仅仅递推式不同而已 t[i][j] = a + b elif a == None and b == None: # a和b都无解,则无解 t[i][j] = None elif a == None: # a无解而b有解,此时一定不能选择物品i t[i][j] = b elif b == None: # a有解而b无解,此时只能选择i t[i][j] = a return t
例子:
- 有不同面额的若干钱币,每种面额只有一个,求表示给定面值的方案数
5.3 求刚好装满背包的最少/多选择物品数
以最少为例:
- 子问题
f[i][j]为前i件物品放入j空间,刚好装满的最少物品数,用None表示无解 - 递推式
f[i][j] = min{ f[i-1][j] (不选i), f[i-1][j-c[i]] + 1 (选i) }
- c[i] > j时一定不能选i,f[i][j] = f[i-1][j]
- 基础子问题的解
- 第一列 f[][0] = 背包空间为0,认为永远是满的,选择物品数为0 = 0
- 第一行(除第一个) f[0][1..totalCapacity] = 没有物品可放,此时除了空间为0的背包,其他背包不存在装满的方案 = None
# 装满时的最少物品数def leastObjFillPack(): t = [[None] * (totalC + 1) for i in range(n+1) ] # [n+1][totalCapacity+1] # 初始化 t[0] = [None] * (totalC + 1) # t[0][1..totalCapacity] = None for i in range(n+1): # t[][0] = 0 t[i][0] = 0 for i in range(1,n+1): for j in range(1,totalC+1): if c[i] > j: # 无法放入第i个物品 -- 必定不选 t[i][j] = t[i-1][j] else: a = t[i-1][j-c[i]] # a: 前i-1个物品放入j-c[i]空间 b = t[i-1][j] # b: 前i-1个物品放入j空间 if a != None and b != None: # a和b都有解,则用递推式 t[i][j] = min(b,a+1) elif a == None and b == None: # a和b都无解,则无解 t[i][j] = None elif a == None: # a无解而b有解,此时一定不能选择物品i t[i][j] = b elif b == None: # a有解而b无解,此时只能选择i t[i][j] = a + 1 return t
例子:
- 有不同面额的若干硬币,每种面额只有一个,求最少需要多少硬币表示给定面值
除了上述几种情况,背包问题还有很多其他变种,不过基本思路和递推过程都是一样的,其核心都在于 1)根据询问的不同找到正确的递推式(min|max|sum...
);2)确定基础子问题的解。
二. 完全背包
1. 基本解法
完全背包指每种物品有无数件,它和01背包的区别只有递推式的一个地方:
- 01 : f[i][j] = max(f[i-1][j],
f[i-1][j-c[i]]
+ v[i]) - 完全:f[i][j] = max(f[i-1][j],
f[i][j-c[i]]
+ v[i])
解释:
考虑第i种物品,若不选,则f[i][j] = f[i-1][j];若选择,意味着至少有一件i物品,考虑这一件物品,剩余j-c[i]空间用来放 0..i种 物品(i依然能放),这部分空间的最大价值为f[i][j-c[i]]。综合即可得到上述公式。
直观的,在递推时,01背包依赖的是上一行的某两个格子(正上方和左侧),完全背包则依赖上一行正上方的格子 + 同一行左侧的某个格子。
2. 空间优化
与01背包类似,但是这里遍历顺序 不要逆序 了,即此时需要从左向右递推。因为01背包递推时f[i-1][j-c[i]]是上一行的值,而完全背包f[i][j-c[i]]则刚好是本行的新值。
三. 多重背包
指的是第i个物品有n[i]件,此时直观上可以将每个物品拆成n[i]个单个物品,转化为01背包问题。但当n[i]比较大时物品件数将急剧增加,为了避免这种情况可以使用 二进制拆分法
,降低拆分后的物品数。
1. 二进制拆分
原理:
一个正整数n可以被分解成1,2,4,…,2^(k-1),n-(2^k+1)(即n-前面数的和), k是满足n>2^k+1的最大整数,且1~n之内的所有整数均可以唯一表示成这些数中某几个数的和的形式。
代码更容易理解些:
var result = [];var n = ..;// 1,2,4,8...直到>n (不包括最后一个数字)for(i=1;i<=n;i*=2){ result.push(i); n-=i;}// n剩下的部分if(n>0) result.push(n);
比如13可以拆分成1/2/4/6,1~13均可由这些数凑成。将件数为13,价值为v的物品拆分成1*v,2*v,4*v,6*v这4件物品后,该物品的任意选择(1件/2件..13件)均可由它们表达,如选择10件该物品 = 选择物品4*v + 选择物品6*v。和简单拆分相比,该拆分方式不但达到了同样的效果,拆分后的个数也由n下降到log2(n),即n的二进制表示的位数。
参考文档
- 背包问题——“01背包”详解及实现(包含背包中具体物品的求解)
- 背包问题应用
- 多重背包的二进制分解思想
- 【无限背包】背包问题
- 背包问题---01背包
- 背包问题--部分背包
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 背包问题
- 程序员的10条建议
- 二分图的一下小证明
- scala文件操作--自动关闭打开的资源,读取properties文件
- java测试运行时间
- Linux文件管理
- 背包问题
- 关于scrollview的一些小问题
- 游戏中 ClippingNode 实现 标题划过的光效 (C++)
- listBox选择多个并进行取值
- mac 查看文件编码
- SqlDataAdapter.FillSchema(DataTable)方法填充数据表的主键和约束
- Linux架构
- DAS,NAS,SAN和ISCSI存储架构介绍
- hdoj2438Turn the corner【三分法】