[总结]背包问题

背包问题指这样一类问题,题意往往可以抽象成:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
就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)\)

参考:
01背包总结
常见的动态规划问题分析与求解
背包问题九讲

posted @ 2019-10-22 10:37  Jamest  阅读(2114)  评论(0编辑  收藏  举报