算法模板-01背包

Posted 周先森爱吃素

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法模板-01背包相关的知识,希望对你有一定的参考价值。

本文转载自Maple博客的算法模板-01背包问题,转载请注明出处。

题目描述

最基本的01背包问题描述如下:
N N N件物品和一个容量为 V V V的背包,放入第 i i i个物品需要耗费的代价为 C i C_i Ci,得到的价值为 W i W_i Wi,求解将哪些物品装入背包可使价值总和最大。

解法

基本思路

最基础的背包问题,其每一件物品只有一个,可以选择放或者不放。一般采用二维动态规划来解决问题,通常定义其子状态为 d p [ i ] [ v ] dp[i][v] dp[i][v],表示前 i i i个物品放入到容量为 v v v的背包中的最大的价值总和。那么这个值是如何得到的呢,对于第 i i i个物品,无非就是两种情况,或者不放。如果不放或者根本放不下( v < C i v < C_i v<Ci),那么问题等同于 i − 1 i - 1 i1个物品放入到容量为 v v v的背包的情况,即:
d p [ i ] [ v ] = d p [ i − 1 ] [ v ] dp[i][v] = dp[i - 1][v] dp[i][v]=dp[i1][v]
如果选择放,那么问题就等同于 i − 1 i - 1 i1个物品放入到容量为 v − C i v - C_i vCi的背包的情况,即:
d p [ i ] [ v ] = d p [ i − 1 ] [ v − C i ] dp[i][v] = dp[i - 1][v - C_i] dp[i][v]=dp[i1][vCi]
最终 d p [ i ] [ v ] dp[i][v] dp[i][v]的取值就两者中更大的一个。

初始化dp数组时的细节

在大多数动态规划解法的题目中,初始化一直是很重要并且较难思考的一件事情。一般在求最优解的背包问题中,有两种问法,一个是要求“恰好装满背包”时的最优解,另一个则是不需要恰好装满背包,只求最优解。
如果是第一种情况,要求恰好装满背包,那么在初始化的时候,除了 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]为0,其它的 d p [ 0 ] [ 1... V ] dp[0][1...V] dp[0][1...V]均设为 − ∞ -\\infty 。说明此时只有容量为0的背包可以在什么也不装的情况下被“恰好装满”,其价值为0,而容量为1…V的背包,无法在没有物品的情况下被装满,没有合法的解,所以设定为 − ∞ -\\infty
对于第二种情况,题目并没有要求背包装满,只是希望价值尽可能大,那么 d p [ 0 ] [ 0... V ] dp[0][0...V] dp[0][0...V]均初始化为0。因为任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始化状态的值也就全部为0了。

背包问题的优化

上述的基本思路中,时间和空间的复杂度均为 O ( V N ) O(VN) O(VN),其中的空间复杂度是可以被优化到 O ( V ) O(V) O(V)的。从 d p [ i ] [ v ] dp[i][v] dp[i][v]的更新方程中不难看出,其实 d p [ i ] dp[i] dp[i]的值,只和 d p [ i − 1 ] dp[i - 1] dp[i1]有关,如下图:

那我们在一开始初始化的时候,只需要初始化 d p [ 0... V ] dp[0...V] dp[0...V]大小的数组即可,每一次对物品的循环( i i i)都只需要更新这一行数组就行。但需要注意的是,更新的时候要逆序更新,即从V更新到0。
举个例子,假如正向更新,访问到了 v = 6 v=6 v=6的时候,你需要通过 n e w _ d p [ 6 ] = m a x ( o l d _ d p [ 6 ] , o l d _ d p [ 3 ] ) new\\_dp[6] = max(old\\_dp[6], old\\_dp[3]) new_dp[6]=max(old_dp[6],old_dp[3]),而由于是正向更新, v = 3 v=3 v=3的值已经被更新成了 n e w _ d p [ 3 ] new\\_dp[3] new_dp[3] o l d _ d p [ 3 ] old\\_dp[3] old_dp[3]已经无法被访问,所以无法更新,所以这一层循环需要你需更新。

题目列表

在这里列举leetcode上四题经典01背包的题目。

416 分割等和子集

原题链接

这题要求判断是否一个数组分成相等的两个子数组。
即选取若干个数字,刚好值为和的一半。
Python代码如下:

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n == 1: return False
        total = sum(nums)
        if total % 2 == 1: return False
        target = total // 2
        maxnum = max(nums)
        if maxnum > target: return False

        dp = [[False] * (target + 1) for _ in range(n)]
        for i in range(n):
            dp[i][0] = True
        dp[0][nums[0]] = True
        for i in range(1, n):
            num = nums[i]
            for j in range(1, target + 1):
                if j < num:
                    dp[i][j] = dp[i - 1][j]
                else:
                    dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num]
        return dp[n - 1][target]

优化空间复杂度的版本如下:

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n < 2:
            return False
        
        total = sum(nums)
        if total % 2 != 0:
            return False
        
        target = total // 2
        dp = [True] + [False] * target
        for i, num in enumerate(nums):
            for j in range(target, num - 1, -1):
                dp[j] |= dp[j - num]

        return dp[target]

474 一和零

原题链接
这题要求给你一个字符串数组,里面的字符串均为0和1组成,例如:strs = [“10”, “0001”, “111001”, “1”, “0”],再给你 m = 5 m=5 m=5 n = 3 n=3 n=3,让你找出0的个数小于 m m m且1的个数小于 n n n的最大子集的长度。
这题同经典背包问题不同的是,它有两重限制,需要采用三维动态规划完成,Python代码如下:

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        k = len(strs)
        if m == 0 and n == 0: return 0
        dp = [[[False] * (n + 1) for _ in range(m + 1)] for __ in range(k + 1)]
        for s in range(k+1):
            dp[s][0][0] = 0
        for i in range(m+1):
            for j in range(n+1):
                dp[0][i][j] = 0
        for s in range(1, k+1):
            for i in range(m + 1):
                for j in range(n + 1):
                    nums0, nums1 = self.count(strs[s - 1])
                    if nums0 <= i and nums1 <= j:
                        dp[s][i][j] = max(dp[s - 1][i][j], dp[s - 1][i - nums0][j - nums1] + 1)
                    else:
                        dp[s][i][j] = dp[s - 1][i][j]
        return dp[k][i][j]

	def count(self, s):
        count0, count1 = 0, 0
        for ss in s:
            if ss == '0':
                count0 += 1
            elif ss == '1':
                count1 += 1
        return count0, count1

虽然是三维动态规划,但其也可以进行空间复杂度的优化,其代码如下:

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        k = len(strs)
        if m == 0 and n == 0: return 0
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        dp[0][0] = 0
        for s in range(1, k + 1):
            for i in range(m, -1, -1):
                for j in range(n, -1, -1):
                    nums0, nums1 = self.count(strs[s - 1])
                    if nums0 <= i and nums1 <= j:
                        dp[i][j] = max(dp[i][j], dp[i - nums0][j - nums1] + 1)
        return dp[m][n]

	def count(self, s):
        count0, count1 = 0, 0
        for ss in s:
            if ss == '0':
                count0 += 1
            elif ss == '1':
                count1 += 1
        return count0, count1

494 目标和

原题链接
题目要求,将给的数组中每个数字前面添加“ + + +”或者“ − - ”,使得整个公式的和为题目给的target,问一共有多少种公式。
例如,对于nums = [1,1,1,1,1], target = 3,则共有5种公式:
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
第一眼看上去这题与背包问题并不一致,需要做一点小小的改变。通过所有数的总和以及给出的target,我们可以计算出所有前面符号为“

以上是关于算法模板-01背包的主要内容,如果未能解决你的问题,请参考以下文章

算法模板-01背包

算法模板-01背包

动态规划背包问题总结:01完全多重与其二进制优化分组背包 题解与模板

算法刷题总结

算法刷题总结

算法刷题总结