01背包问题🎒

01 背包模板

网上讲刷题的文章太多了,K神(Krahets),灵神(灵茶山艾府),卡尔(代码随想录),左神(左程云)这几个大佬讲的都很好,感兴趣的自己去看,本文只是无脑刷题笔记而已。、

背包问题我个人推荐大家去看一下代码随想录写的教程,通俗易懂一文上手。

01背包问题是一个经典的组合优化问题,它可以被描述为:给定一组物品,每个物品有一个重量和一个价值,同时给定一个固定的背包容量,要求在不超过背包容量的前提下,选择一些物品装入背包,使得装入的物品总价值最大化。

问题的名称源于每个物品只有两种选择:要么装入背包(取1),要么不装入背包(取0)。因此,该问题的解可以用一个长度为n的二进制数组表示,其中n是物品的数量。数组的第i个元素表示第i个物品是否被选中。

01背包要点:物品不能重复选,只有"选"或者"不选"。

模板

py 复制代码
dp = []*背包容量


# 双层for循环,外层遍历物品,内层逆序遍历背包容量

for i in 物品:
    for j in 背包:(逆序遍历)
        dp[j] = dp[j-i] + i价值

416. 分割等和子集 - 力扣(LeetCode)

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

ini 复制代码
输入: nums = [1,5,11,5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

ini 复制代码
输入: nums = [1,2,3,5]
输出: false
解释: 数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100
PY 复制代码
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        '''
        总和:s
        选好的子集和:c
        未选的子集和:s-c
        s-c = c --> s/2 = c 的时候True
        转化为01背包问题,从数中选出和为s/2的数
        01背包一定先遍历物品,再**倒序**遍历背包容量

        '''
        s = sum(nums)
        if s%2 == 1:
            return False
        target = s//2
        dp = [0]*(target+1)
        for i in range(len(nums)):
            for j in range(target,nums[i]-1,-1):
                dp[j] = max(dp[j],dp[j-nums[i]] + nums[i])
        return dp[-1] == target

这段代码实现了一个判断给定数组 nums 是否可以分割成两个子集,使得这两个子集的元素和相等(即可以等分为两个和相等的子集)。这是一个典型的0-1背包问题的变种。

代码中的 canPartition 函数使用动态规划的方法来解决这个问题。下面是对代码的分析:

  1. 计算数组元素总和 s,如果 s 为奇数,直接返回 False,因为奇数无法等分成两个相等的子集。
  2. 如果 s 为偶数,计算出目标和 targets // 2,因为如果存在两个子集的和相等,那么每个子集的和就应该是 target
  3. 创建一个长度为 target + 1 的动态规划数组 dp,其中 dp[i] 表示是否可以使用给定的数组中的元素凑出和为 i
  4. 开始遍历数组 nums 的每个元素,对于每个元素 nums[i],在动态规划数组 dp 中,倒序遍历从 targetnums[i](包括 nums[i])的范围,这是因为在更新 dp[j] 时需要用到之前已经计算过的 dp[j-nums[i]],确保每个元素只能被使用一次。
  5. 更新 dp[j],将其更新为 max(dp[j], dp[j-nums[i]] + nums[i]),表示在考虑当前元素 nums[i] 时,是否可以凑出和为 j。这里的 max 表示在考虑选取当前元素和不选取当前元素两种情况中选择最大的值。
  6. 最终,返回 dp[-1] == target,即判断是否存在一种选取数组中的元素,凑出和为 target,从而分割成两个和相等的子集。

通过动态规划的思想,将原问题转化为一个经典的0-1背包问题,并通过填表的方式逐步求解,最终判断是否可以将数组分割成两个和相等的子集。

1049. 最后一块石头的重量 II - 力扣(LeetCode)

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头 ,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

ini 复制代码
输入: stones = [2,7,4,1,8,1]
输出: 1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

ini 复制代码
输入: stones = [31,26,33,21,40]
输出: 5

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 100
PY 复制代码
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        '''
        和416分割等和子集是一样的!
        能否完全粉碎就是看能否找到两个子集和相等
        如果和不相等,返回两个子集最小的差值
        '''
        s = sum(stones)
        # 这里不能用 s%2==0 返回True,因为可能相差2
        target =  s // 2
        n = len(stones)
        dp = [0]*(s+1)
        for i in range(n):
            for j in range(target,stones[i]-1,-1):
                dp[j] = max(dp[j],dp[j-stones[i]]+stones[i])
        return s-2*dp[target]

在给定一组石头的重量 stones 时,你需要将这些石头分成两堆,使得这两堆的重量尽可能接近。最终返回两堆石头的重量差。

代码中的 lastStoneWeightII 函数同样使用了动态规划的思想来解决这个问题。下面是对代码的分析:

  1. 计算所有石头的总重量 s
  2. 计算目标和 targets // 2。和之前一样,这是因为问题可以转化为寻找一个子集,使得该子集的和尽可能接近 target
  3. 初始化一个长度为 s + 1 的动态规划数组 dp,其中 dp[i] 表示是否可以用石头的重量凑出和为 i
  4. 开始遍历数组 stones 的每个元素,对于每个元素 stones[i],在动态规划数组 dp 中,倒序遍历从 targetstones[i](包括 stones[i])的范围,这是因为在更新 dp[j] 时需要用到之前已经计算过的 dp[j-stones[i]],确保每个石头只能被使用一次。
  5. 更新 dp[j],将其更新为 max(dp[j], dp[j-stones[i]] + stones[i]),表示在考虑当前石头 stones[i] 时,是否可以凑出和为 j。这里的 max 表示在考虑选取当前石头和不选取当前石头两种情况中选择最大的值。
  6. 最终,返回 s - 2 * dp[target],这表示将石头分成两堆,一堆的重量是 dp[target],另一堆的重量就是总重量 s 减去第一堆的重量。

这段代码通过动态规划的方式,将原问题转化为一个类似0-1背包问题,找到一个子集的和尽可能接近 target,然后通过计算差值得出将石头分成两堆后的最小重量差。

494. 目标和 - 力扣(LeetCode)

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

ini 复制代码
输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

ini 复制代码
输入: nums = [1], target = 1
输出: 1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000
PY 复制代码
class Solution:
    def findTargetSumWays(self, nums, target):
        '''
        数组和 s
        整数和 p
        负数和 n = s-p
        target = s-n = p-(s-p)
        --> target + s = 2p
        --> p = (s + target) // 2
        --> 01背包
        背包容量p
        dp[j]表示当前选数之和为j的方案数,
        状态转移方程为:dp[j] += dp[j-nums[i]],
        表示选当前数nums[i]和不选nums[i]两种情况。
        '''
        s = sum(nums)
        n = len(nums)
        if abs(target) > abs(s) or (s + target) % 2 == 1:
            return 0
        target = (s+target)//2
        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(n):
            for j in range(target, nums[i] - 1, -1):
                dp[j] += dp[j - nums[i]]
        return dp[-1]

给定一个非空整数数组 nums 和一个目标整数 target,你需要找出有多少种将数组中的元素通过添加正号或负号相加,可以得到目标整数 target

代码中的 findTargetSumWays 函数使用动态规划的方法来解决这个问题。下面是对代码的分析:

  1. 计算数组元素总和 s
  2. 判断如果目标整数的绝对值大于总和的绝对值,或者 (s + target) 为奇数,直接返回 0,因为无法通过添加正号或负号得到目标整数。
  3. 根据分析,我们可以得出 target + s = 2p,其中 p 为一组元素前面添加正号的和,这个问题被转化为一个背包问题,寻找和为 p 的组合数。
  4. 创建一个长度为 target + 1 的动态规划数组 dp,其中 dp[j] 表示选取一些元素和为 j 的方案数。
  5. 初始化 dp[0]1,表示当不选取任何元素时,和为 0 的方案数为 1
  6. 开始遍历数组 nums 的每个元素,对于每个元素 nums[i],在动态规划数组 dp 中,倒序遍历从 targetnums[i](包括 nums[i])的范围,这是因为在更新 dp[j] 时需要用到之前已经计算过的 dp[j-nums[i]],确保每个元素只能被使用一次。
  7. 更新 dp[j],将其更新为 dp[j] += dp[j - nums[i]],表示在考虑选取当前数 nums[i] 和不选取当前数 nums[i] 两种情况下,和为 j 的方案数之和。
  8. 最终,返回 dp[-1],即在给定数组中通过添加正号或负号得到目标整数 target 的方案数。

这段代码通过动态规划的思想,将问题转化为0-1背包问题,寻找和为特定值的方案数,然后通过动态规划数组逐步计算得出最终的结果。

474. 一和零 - 力扣(LeetCode)

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

arduino 复制代码
输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出: 4
解释: 最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

arduino 复制代码
输入: strs = ["10", "0", "1"], m = 1, n = 1
输出: 2
解释: 最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0''1' 组成
  • 1 <= m, n <= 100
PY 复制代码
class Solution:
    def findMaxForm(self, strs, m: int, n: int) -> int:
        '''
        01背包问题
        背包容量 0-m 1-n
        dp[z][o]表示最多有z个0和o和1的子集的数量
        '''
        dp = [[0]*(n+1) for _ in range(m+1)]

        for s in strs:
            o = s.count('1')
            z = s.count('0')
            for i in range(m,z-1,-1):
                for j in range(n,o-1,-1):
                    dp[i][j] = max(dp[i][j], dp[i-z][j-o] + 1)
        return dp[-1][-1]

给定一个由仅含有字符 '0' 和 '1' 的字符串组成的数组 strs,以及两个非负整数 mn,表示可以使用的 '0' 和 '1' 的数量上限,你需要从数组中选取字符串,使得选取的字符串的 '0' 和 '1' 的数量分别不超过 mn,并返回最大的选取字符串数量。

代码中的 findMaxForm 函数使用了动态规划的思想来解决这个问题。下面是对代码的分析:

  1. 创建一个二维动态规划数组 dp,其中 dp[z][o] 表示最多可以使用 z 个 '0' 和 o 个 '1' 的子集的数量。
  2. 对于每个字符串 s,统计其中 '0' 和 '1' 的数量,分别记为 zo
  3. 使用嵌套循环遍历背包容量的范围,即从 mz 以及从 no。这是因为在计算 dp[i][j] 时,需要用到 dp[i-z][j-o],而为了避免重复计算,需要倒序遍历。
  4. 更新 dp[i][j],将其更新为 max(dp[i][j], dp[i-z][j-o] + 1),表示在考虑选取当前字符串时,可以将其加入到符合条件的子集中,并计算新的子集数量。
  5. 最终,返回 dp[-1][-1],表示在给定限制条件下,最大的选取字符串数量。

总的来说,这段代码使用动态规划的思想,将问题转化为一个0-1背包问题,通过填表的方式逐步求解,得出最大的选取字符串数量。

相关推荐
算法小白(真小白)2 小时前
低代码软件搭建自学第二天——构建拖拽功能
python·低代码·pyqt
唐小旭2 小时前
服务器建立-错误:pyenv环境建立后python版本不对
运维·服务器·python
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
Chinese Red Guest3 小时前
python
开发语言·python·pygame
骑个小蜗牛3 小时前
Python 标准库:string——字符串操作
python
xiaoshiguang34 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡4 小时前
【C语言】判断回文
c语言·学习·算法
别NULL4 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇4 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯