背包问题指这样一类问题,题意往往可以抽象成:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
就ACM或者其它算法竞赛而言,背包问题可以分为8种类型,其中最基础的是0/1背包问题。作为动态规划的典型问题,其状态转移方程往往需要认真理解并能自行推出。这八种问题分别为:0/1背包问题、完全背包问题、多重背包问题、混合三种背包问题、二维费用背包问题、分组背包问题、有依赖的背包问题、求背包问题的方案总数。
01背包问题
给定\(n\)种物品和一个容量为\(C\)的背包,物品\(i\)的重量是\(w_i\),其价值为\(v_i\)。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
使用动态规划,阶段就是“物品的件数”,状态就是“背包剩下的容量”,那么很显然 \(f[ i , v ]\) 就设为从前\(i\)件物品中选择放入容量为\(j\)的背包最大的价值。那么状态转移方程为:
\[
f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+c[i])
\]
# 动态规划 时间: O(n*C) 空间:O(n*C) class Solution(): def knapSack(self,w,v,C): n = len(w) dp = [[0 for j in range(C+1)] for i in range(n+1)] for i in range(n+1): for j in range(C+1): if i==j==0: dp[i][j] = 0 elif j==0: dp[i][0] = 0 elif i==0: dp[0][j] = -float('inf') else: if w[i-1]>j: dp[i][j] = dp[i-1][j] else: dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]) return dp[-1][-1] v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] C = 12 res = Solution().knapSack(w,v,C) print('res',res)
实际上,空间可优化到\(O(C)\):
# 动态规划 空间优化 时间: O(n*C) 空间:O(C) class Solution(): def knapSack(self,w,v,C): n = len(w) dp = [0 for i in range(C+1)] for i in range(1,n+1): # 逆序,避免要使用的子状态收到影响。 for j in reversed(range(w[i-1],C+1)): #注意:当dp[j]依赖于<j的数,需要先更新后面的,即j从大向下更新 dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] C = 12 res = Solution().knapSack(w,v,C) print('res',res)
另外,当\(C\)很大时,也可以按照价值来进行dp,算法复杂度是\(O(n*m)\) .其中\(m\)是背包价值的总和,所以很显然,背包价值范围很小或者\(C\)极大,就用这个方法(另一种情况,当w为小数时,也应该用这种方法)。
#动态规划 class Solution(): def knapSack(self,w,v,C): sum_v = sum(v) n = len(w) dp = [float('inf') for i in range(sum_v+1)] dp[0] = 0 for i in range(1,n+1): for j in reversed(range(v[i-1],sum_v+1)): # dp[i] = k,表示的是,已经装了价值为 i 的物品所需要的最小体积k。 dp[j] = min(dp[j],dp[j-v[i-1]]+w[i-1]) for i in reversed(range(sum_v+1)): if dp[i] <= C: return i v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] C = 12 res = Solution().knapSack(w,v,C) print('res',res)
还有一种DFS+剪枝的方法,适用于物品个数比较少,但是背包容量C和物品价值v都太大了
# DFS+剪枝 class Solution(): def knapSack(self,w,v,C): n = len(w) sum_v = v sum_w = w for i in range(1,len(v)): sum_v[i] = sum_v[i-1]+v[i] sum_w[i] = sum_w[i-1]+w[i] self.ans = 0 def dfs(a,weight,val): if(val > self.ans): self.ans = val if a==-1: return for i in reversed(range(a+1)): if weight+sum_w[i]<=C and val+sum_v[i]>self.ans: #由于数据是排列好的,从大的数据开始DFS(这一点很重要,是常用的剪枝方法),用辅助数组存放前i项的和,只有weight+sum_w[i]<=C and val+sum_v[i]>self.ans的分支才有可能产生更大的值,这样剪枝来降低时间复杂度。 dfs(i-1,weight+w[i],val+v[i]) dfs(n-1,0,0) return self.ans v = [2, 3, 6, 8, 7, 10] w = [1, 2, 2, 4, 5, 6] C = 12 res = Solution().knapSack(w,v,C) print('res',res)
实际使用中,需要根据真实场景选择不同的方法。
完全背包
有\(N\)种物品和一个容量为\(C\)的背包,每种物品都有无限件可用。第\(i\)种物品的费用是\(w[i]\),价值是\(v[i]\)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
同样的,使用动态规划,可以将二位空间优化到一维空间。
# 动态规划 二维空间-一维空间 class Solution(): def knapSack(self,w,v,C): n = len(w) dp = [[0 for j in range(C+1)] for i in range(n+1)] for i in range(1,n+1): for j in range(1,C+1): if w[i-1]>j: dp[i][j] = dp[i-1][j] else: dp[i][j] = max(dp[i-1][j],dp[i][j-w[i-1]]+v[i-1]) return dp[-1][-1] def knapSack2(self,w,v,C): n = len(w) dp = [0 for i in range(C+1)] for i in range(1,n+1): for j in range(w[i-1],C+1): dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] w = [2, 3, 7] v = [1, 1, 1] C = 11 res = Solution().knapSack(w,v,C) print('res',res)
还有一种,要求价值总和最大,且总重量恰好等于背包容量C
class Solution(): def knapSack(self,w,v,C): n = len(w) dp = [-float('inf') for i in range(C+1)] dp[0] = 0 for i in range(1,n+1): for j in range(w[i-1],C+1): dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] if dp[-1]!=-float('inf') else -1 w = [5 ,6, 7] v = [1, 1, 1] C = 9 res = Solution().knapSack(w,v,C) print('res',res)
假设价值总和最小,且总重量恰好等于背包容量C:
#类似于 Leetcode322. Coin Change class Solution(): def knapSack(self,w,v,C): n = len(w) dp = [float('inf') for i in range(C+1)] dp[0] = 0 for i in range(1,n+1): for j in range(w[i-1],C+1): if dp[j-w[i-1]]!='float': dp[j] = min(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] if dp[-1]!=float('inf') else -1 w = [2, 3, 7] v = [1, 1, 1] C = 11 res = Solution().knapSack(w,v,C) print('res',res)
多重背包
多重背包(MultiplePack): 有\(N\)种物品和一个容量为\(C\)的背包。第\(i\)种物品最多有\(n[i]\)件可用,每件费用是\(w[i]\),价值是\(v[i]\)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
可将多重背包进行二进制分解,然后类似01背包问题求解
class Solution(): def knapSack(self,w,v,n,C): len_ = len(w) w_ = [] v_ = [] for i in range(len_): j = 1 while j<=n[i]: w_.append(w[i]*j) v_.append(v[i]*j) n[i] -= j j <<= 1 if n[i]>0: w_.append(w[i]*n[i]) v_.append(v[i]*n[i]) return self.knapSack01(w_,v_,C) #01背包问题 def knapSack01(self,w,v,C): n = len(w) dp = [0 for i in range(C+1)] dp[0] = 0 for i in range(1,n+1): for j in reversed(range(w[i-1],C+1)): dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] n = [1, 1, 1, 1, 1, 8] v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] C = 12 res = Solution().knapSack(w,v,n,C) print('res',res)
混合三种背包问题
有的物品只可以取一次(01 背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。
可以转化为01背包和完全背包的混合问题。
class Solution(): def knapSack(self,w,v,n,C): len_ = len(w) w_ = [] v_ = [] n_ = [] for i in range(len_): if n[i] == -1: w_.append(w[i]) v_.append(v[i]) n_.append(-1) else: j = 1 while j<=n[i]: w_.append(w[i]*j) v_.append(v[i]*j) n_.append(1) n[i] -= j j <<= 1 if n[i]>0: w_.append(w[i]*n[i]) v_.append(v[i]*n[i]) n_.append(1) print((w_,v_,n_,C)) return self.knapSack01(w_,v_,n_,C) #01背包和完全背包的混合问题 def knapSack01(self,w,v,n,C): dp = [0 for i in range(C+1)] dp[0] = 0 for i in range(1,len(w)+1): if n[i-1] == 1: for j in reversed(range(w[i-1],C+1)): dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) else: for j in range(w[i-1],C+1): dp[j] = max(dp[j],dp[j-w[i-1]]+v[i-1]) return dp[-1] n = [1, 1, 1, 1, 6, -1] v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] C = 12 res = Solution().knapSack(w,v,n,C) print('res',res)
二维费用的背包问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。 问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为\(a[i]\)和\(b[i]\)。两种代价可付出的最大值 (两种背包容量)分别为\(V\)和\(U\)。物品的价值为\(w[i]\)。
最常见的是对物品个数的限制。
class Solution(): def knapSack(self,w1,w2,v,g,C1,C2): n = len(w1) dp = [[0 for k in range(C2+1)]for j in range(C1+1)] for i in range(1,n+1): # 逆序,避免要使用的子状态收到影响。 for j in reversed(range(w1[i-1],C1+1)): #注意:当dp[j]依赖于<j的数,需要先更新后面的,即j从大向下更新 for k in reversed(range(w2[i-1],C2+1)): dp[j][k] = max(dp[j][k],dp[j-w1[i-1]][k-w2[i-1]]+v[i-1]) # print('dp',dp) return dp[-1][-1] v = [8, 10, 6, 3, 7, 2] w1 = [4, 6, 2, 2, 5, 1] w2 = [2, 4, 3, 1, 2, 2] C1 = 12 C2 = 6 res = Solution().knapSack(w1,w2,v,g,C1,C2) print('res',res)
分组的背包问题
有\(N\)件物品和一个容量为\(V\)的背包。第\(i\)件物品的费用是\(c[i]\),价值是\(w[i]\)。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。 求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
# 动态规划 时间: O(n*C) 空间:O(n*C) class Solution(): def knapSack(self,w,v,g,C): w_ = {} v_ = {} n = len(w) groups_num = 0 #分租数目 for i in range(n): if g[i] not in w_: w_[g[i]] = [] v_[g[i]] = [] groups_num += 1 w_[g[i]].append(w[i]) v_[g[i]].append(v[i]) dp = [0 for i in range(C+1)] for i in range(groups_num): for j in reversed(range(C+1)): for p in range(len(w_[i])): if (j >= w_[i][p]): dp[j] = max(dp[j], dp[j - w_[i][p]] + v_[i][p]) return dp[-1] v = [8, 10, 6, 3, 7, 2] w = [4, 6, 2, 2, 5, 1] g = [0, 0, 1, 1, 2, 2] #组数 组号需要从0开始编号 C = 12 res = Solution().knapSack(w,v,g,C) print('res',res)
有依赖的背包问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品\(i\)依赖于物品\(j\),表示若选物品\(i\),则必须选物品\(j\)。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
泛化物品
在背包容量为\(V\)的背包问题中,泛化物品是一个定义域为0... V中的整数的函数h,当分配给它的费用为\(v\)时,能得到的价值就是\(h(v)\)。