背包问题基础与应用

news2024/9/21 18:39:05

背包问题

理论基础

01背包

背包中的每个物品只能用一次

物品编号重量价值
物品1115
物品2320
物品3430

定义dp[i][j]表示从下标0-i的物品中任取,放进容量为j的背包的最大价值

初始化

背包问题初始化
dp = [[0] * (bag_size + 1) for _ in range(len(weight))]
for j in range(1, bag_size + 1):
    if j >= weight[0]:
        dp[0][j] = value[0]

行为物品,列为背包容量。背包容量从0开始,所以列数为bag_size + 1

背包问题0

转移方程dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

将物品放入背包有两种选择:

  • 不放当前物品,则延续前一物品的状态,用dp[i-1][j]表示
  • 放入当前物品,则腾出空间,将当前物品放进去,维护dp数组,用dp[i-1][j-weight[i]] + value[i])表示

遍历顺序

背包问题1

到底是先遍历物品呢?还是先遍历背包?

根据上图,dp[i][j]依赖于dp[i-1][j-w]和dp[i-1][j],即左上角和正上方,画图可知两种遍历顺序都可以

1.先遍历物品,后遍历背包

    for i in range(1, len(weight)):  # 遍历物品
        for j in range(1, bag_size + 1):  # 遍历背包
            if j < weight[i]:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

2.先遍历背包,后遍历物品

    for j in range(1, bag_size + 1):
        for i in range(1, len(weight)):
            if j < weight[i]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

完整代码:

初始化的代码可以省略,类比在物品1之前,加了一个质量为0价值为0的物品0

def zero_one_bag(bag_size, weight, value):
    dp = [[0] * (bag_size + 1) for _ in range(len(weight))]
    for i in range(len(weight)):
        for j in range(1, bag_size + 1):
            if j < weight[i]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
    return dp[-1][-1]


if __name__ == "__main__":
    bag_size = 4
    weight = [1, 3, 4]
    value = [15, 20, 30]
    zero_one_bag(bag_size, weight, value)

状态压缩:

背包问题0

仔细观察转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

发现dp[i][j]的取值只依赖于上一行的值,而在for循环遍历过程中,上一行的数据已经计算过了,我们只需要把dp[i-1][j]和dp[i-1][j-w]的数据保留下来

具体做法:反向遍历背包,把dp[j]左边的数据当作备忘录,从右往左遍历的过程就是更新备忘录的过程

def zero_one_bag(bag_size, weight, value):
    dp = [0] * (bag_size + 1)
    for i in range(len(weight)):
        for j in range(bag_size, weight[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    return dp[-1]

对比二维数组和一维数组发现少了两行代码:

if j < weight[i]:
    dp[i][j] = dp[i - 1][j]

因为一维数组从上往下滚动的过程中,如果在当前行不做更新,相当于沿用了上一行的数据,所以可以省略


完全背包

但凡刚才把滚动数组的遍历顺序写反了,完全背包的代码其实就写出来了

正向遍历滚动数组,就变成了完全背包(每个物品可以用无限次):

  • 反向遍历看到的是上一行,dp[i-1][j - weight[i]] + value[i]

  • 正向遍历看到的是当前行,dp[i][j - weight[i]] + value[i]

背包问题2

一维数组

def full_bag(bag_size, weight, value):
    dp = [0] * (bag_size + 1)
    for i in range(len(weight)):
        for j in range(weight[i], bag_size + 1):
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

二维数组

def full_bag(bag_size, weight, value):
    dp = [[0] * (bag_size + 1) for _ in range(len(weight))]
    for i in range(len(weight)):
        for j in range(1, bag_size + 1):
            if j < weight[i]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
    return dp[-1][-1]

应用举例

分割等和子集

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

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


数组总和为total,判断数组能否用两个容量相等的背包装下

  • 数组中的每个元素就是物品,重量和价值都为nums[i]
  • 每个物品只能用一次
  • 只需要判断一个背包的情况就行了

什么时候背包取得最大价值?答案是背包装满的时候,所以我们只需要判断背包的最大价值是否等于背包容量

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2:
            return False
        target = total // 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

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

问题转换:将石头分成质量尽可能接近的两堆,求两堆的质量之差

  • 数组中的元素是物品,质量和价值都是nums[i]
  • 背包的容量为total//2,尽可能的装满背包,装的越满,质量差越小,刚好装满时质量差为0
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        total = sum(stones)
        target = total // 2
        dp = [0] * (target + 1)
        for stone in stones:
            for i in range(target, stone - 1, -1):
                dp[i] = max(dp[i], dp[i-stone] + stone)
        
        return total - 2 * dp[-1]

组合数

494. 目标和 - 力扣(Leetcode)

问题可以转换为:

  1. 数组nums中任取元素,一共有多少种和为target的组合
  2. 数组元素是物品,重量为nums[i],目标和是背包容量,每个物品只能用一次,问把背包装满有多少种方法
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if abs(target) > total or (total + target) % 2:
            return 0
        bag_size = (total + target) // 2
        dp = [0] * (bag_size + 1)
        dp[0] = 1
        for i in range(len(nums)):
            for j in range(bag_size, nums[i]-1, -1):
                dp[j] += dp[j-nums[i]]
        return dp[-1]

2400. 恰好移动 k 步到达某一位置的方法数目 - 力扣(Leetcode)

每一步都是1个单位,要么向右要么向左

问题:从数轴上点x移动到y,总共移动k步,有多少种方法?

  • 假设向左一共移动a步,向右一共移动b步,则a + b = k, a - b = y - x,得到a = (k + y - x) // 2
  • 问题转换为 C k a C_k^a Cka,求k个1中组合为a的方法数
  • 物品的重量是1,求背包装满的方法数
class Solution:
    def numberOfWays(self, startPos: int, endPos: int, k: int) -> int:
        if ( k + endPos - startPos) % 2:
            return 0
        target = ( k + endPos - startPos) // 2
        dp = [0] * (target + 1)
        dp[0] = 1
        for _ in range(k):
            for j in range(target, 0, -1):
                dp[j] += dp[j-1]
        return dp[-1] % (10**9+7)

二维背包

474. 一和零 - 力扣(Leetcode)

问题转换:容量为m个0,n个1的背包,最多能装下多少物品。

背包的维度是二维,滚动数组的维度也是二维。每个物品只能用一次,依然是倒序遍历

  • dp[i][j]表示容量为i个0,j个1的背包的最大物品数
  • dp[i][j] = max(dp[i][j], dp[i-cnt0][j-cnt1] + 1)
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        # dp[i][j]表示容量为i个0,j个1的背包的最大物品数
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for x in strs:
            cnt0 = x.count('0')
            cnt1 = len(x) - cnt0
            for i in range(m, cnt0 - 1, -1):
                for j in range(n, cnt1 - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i-cnt0][j-cnt1] + 1)
        return dp[m][n]

这一题如果不用滚动数组,dp数组应该是三维的

排列组合

518. 零钱兑换 II - 力扣(Leetcode)

硬币就是物品,金额就是背包容量

  • dp[i]表示装满背包的方法数,初始化dp[0]=1
  • dp[j] += dp[j-coins[i]],每新来一个硬币,都要把当前硬币的贡献加上去
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        # dp[i]表示凑出i的方法数
        dp = [0] * (amount + 1)
        dp[0] = 1
        for i in range(len(coins)):
            for j in range(coins[i], amount + 1):
                dp[j] += dp[j-coins[i]]
        return dp[-1]

377. 组合总和 Ⅳ - 力扣(Leetcode)

完全背包求排列数

滚动数组:先遍历背包,在遍历物品。因为背包容量每次更新,都要根据前面所有的物品去做选择

7034d260ab8a99911fe4530e836c9c52.jpg

总结一下:

  • 求组合数和排列数的内外层循环的遍历顺序相反,其他不变。
  • 组合问题,看到的是当前物品和前面使用过的物品,限制是物品;排列问题,每次都能看到所有物品,限制是背包容量。
# 排列
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1
        for j in range(1, target + 1):
            for i in range(len(nums)):
                if nums[i] <= j:
                    dp[j] += dp[j-nums[i]]
        return dp[-1]
    
# 组合
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1
        for x in nums:
            for i in range(x, target + 1):
                dp[i] += dp[i-x]
        return dp[-1]

扩展:

如果要穷举所有的排列数或者组合数呢,那就得用回溯算法了,暴力的尽头是递归。

穷举排列

class Solution:
    def __init__(self):
        self.path = []
        self.res = []

    def combinationSum4(self, nums: List[int], target: int) -> List[int]:

        def backTrace(cur):
            if cur == target:
                self.res.append(self.path[:])
                return

            for x in nums:
                if x + cur > target:
                    continue
                self.path.append(x)
                backTrace(cur + x)
                self.path.pop()

        backTrace(0)
        return self.res

穷举组合

那就需要去重了,每个for循环中,只用之前没有用过的数字

class Solution:
    def __init__(self):
        self.path = []
        self.res = []

    def combinationSum4(self, nums: List[int], target: int) -> List[int]:

        def backTrace(index, cur):
            if cur == target:
                self.res.append(self.path[:])
                return

            for i in range(index, len(nums)):
                if nums[i] + cur > target:
                    continue
                self.path.append(nums[i])
                backTrace(i, cur + nums[i])
                self.path.pop()

        backTrace(0, 0)
        return self.res

70. 爬楼梯 - 力扣(Leetcode)

爬楼梯其实也是排列问题,原题太简单了,把问题改成每次可以爬1~m个台阶,问有多少种爬楼梯的方式?

class Solution:
    def climbStairs(self, m: int, n: int) -> int:
        # 每次可以爬1~m个台阶
        dp = [0] * (n + 1)
        dp[0] = 1
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if j <= i:
                    dp[i] += dp[i-j]
        return dp[-1]

组合计数

279. 完全平方数 - 力扣(Leetcode)

完全背包,组合问题,先后遍历顺序都可以

class Solution:
    def numSquares(self, n: int) -> int:
        # dp[i] = min(dp[i-x**2], dp[i])
        dp = [n] * (n + 1)
        dp[0] = 0
        for i in range(1, n + 1):
            x = 1
            while x * x <= i:
                dp[i] = min(dp[i], dp[i-x*x] + 1)
                x += 1
        return dp[-1]

其他

139. 单词拆分 - 力扣(Leetcode)

单词是物品,句子是背包,每个单词可以用无限次,单词有顺序,所以是完全背包求排列,先遍历句子,再遍历单词

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True
        for i in range(1, n + 1):
            r = i
            for w in wordDict:
                l = i - len(w)
                if l >= 0:
                    dp[r] = (dp[l] and s[l:r] == w) or dp[r]
        return dp[-1]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431807.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java程序内存占用优化实践

目录背景堆内存调整内存还会继续上涨减少线程数量TomcatDubboLogback野线程背景 上了微服务的当&#xff0c;喜欢将服务各种拆分&#xff0c;公有云模式下服务器比较多&#xff0c;还能玩得转。到了私有化部署&#xff0c;有的客户连个技术人员都没有&#xff0c;只想一键启动…

java——《面试题——基础篇》

1、 Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象&#xff08;Java最重要的特性&#xff0c;让程序耦合度更低&#xff0c;内聚性更高&#xff09; 3、与平台无关性&#xff08;JVM是Java跨平台使用的根本&#xff09; 4、可靠安全 5、支持多线程 2、面向对象…

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCVSharp实现Mono12和Mono16位深度的图像保存(C#)

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCVSharp实现Mono12和Mono16位深度的图像保存&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机保存位深度12/16位图像的技术背景代码案例分享1&#xff1a;引用合适的类文件2&#xff1a;BGAPI SDK在图像回调中联合O…

【GCU体验】基于PaddlePaddle + GCU跑通模型并测试GCU性能

一、环境 地址&#xff1a;启智社区:https://openi.pcl.ac.cn/ 二、计算卡介绍 云燧T20是基于邃思2.0芯片打造的面向数据中心的第二代人工智能训练加速卡&#xff0c;具有模型覆盖面广、性能强、软件生态开放等特点&#xff0c;可支持多种人工智能训练场景。同时具备灵活的可…

STM32理论 —— 定时器、时钟

文章目录 1. 定时器1.1 分类与简介1.1.1 分类与主要功能特点1.1.2 三种常用的定时器简介1.1.3 三种计数模式1.1.4 定时器计数原理 1.2 时钟来源1.3 通用定时器简介1.4 计数溢出时间公式1.4 定时器中断的原理1.5 输入捕获1.6 核心代码1.6.1 通用定时器初始化1.6.2 高级定时器初始…

【Python_Scrapy学习笔记(十三)】基于Scrapy框架的图片管道实现图片抓取

基于Scrapy框架的图片管道实现图片抓取 前言 本文中介绍 如何基于 Scrapy 框架的图片管道实现图片抓取&#xff0c;并以抓取 360 图片为例进行展示。 正文 1、Scrapy框架抓取图片原理 利用 Scrapy 框架提供的图片管道类 ImagesPipeline 抓取页面图片&#xff0c;在使用时需…

领域驱动设计理论实践

战略设计 战略设计是将“混沌”解构成“清晰”的过程&#xff0c;在该过程从开始到结束的历程之中&#xff0c;我们会划分出领域、界定通用语言范围、确定出系统限界上下文以及上下文之间的映射方式。 领域划分 战略设计在领域驱动设计中起着关键作用&#xff0c;因为其决定了…

使用Bazel构建前端Sass

注&#xff1a;本文假设对Bazel有一定的了解。本文基于Bazel 4.2.2 版本 在web前端领域&#xff0c;前端样式&#xff0c;web浏览器只认CSS样式语言。而CSS样式语言又过于低级。于是有人发明了更高级的语言&#xff1a;Sass[1]&#xff0c;用于生成CSS代码。 这样的方案&#x…

【C++】队列模拟问题

文章目录队列模拟问题12.7.1 ATM问题12.7.2 队列类12.7.3 Queue类的接口12.7.4 **Queue类的实现**12.7.5 是否需要其他函数&#xff1f;12.7.6 Customer类queue.hqueue.cpp12.7.7 ATM模拟main.cpp队列模拟问题 12.7.1 ATM问题 Heather银行打算在Food Heap超市开设一个自动柜员…

【C++STL精讲】vector的基本使用与常用接口

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;vector是什么&#xff1f;&#x1f337;vector的基本使用&#x1f337;vector常用函数接口&#x1f490;专栏导读 &#x1f338;作者简介&#xff1a;花想云&#xff0c;在读本科生一枚&#xff0c;致力于 C/C…

HAL库版FreeRTOS(上)

目录 FreeRTOS 简介初识FreeRTOS什么是FreeRTOS?为什么选择FreeRTOS&#xff1f;FreeRTOS 的特点商业许可 磨刀不误砍柴工查找资料FreeRTOS 官方文档Cortex-M 架构资料 FreeRTOS 源码初探FreeRTOS 源码下载FreeRTOS 文件预览 FreeRTOS 移植FreeRTOS 移植移植前准备添加FreeRTO…

浏览器断点调试说明

断点调试 断点调试面板 功能按钮介绍 描述&#xff1a;继续执行脚本 或者叫&#xff08;逐过程执行&#xff09; 快捷键 &#xff08;F8&#xff09;或者是&#xff08;Ctrl\&#xff09; 作用&#xff1a;打断点了的地方&#xff08;比如有是三个断点地方&#xff09;就会 第一…

大数据能力提升项目|学生成果展系列之四

导读 为了发挥清华大学多学科优势&#xff0c;搭建跨学科交叉融合平台&#xff0c;创新跨学科交叉培养模式&#xff0c;培养具有大数据思维和应用创新的“π”型人才&#xff0c;由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…

13.vue-cli

单页面应用程序&#xff1a;所有的功能只在index.html中完成 vue-cli是vue版的webpack 目录 1 安装vue-cli 2 创建项目 3 使用预设 4 删除预设 5 开启项目 6 项目文件内容 6.1 node_moduls 中是项目依赖的库 6.2 public 6.2.1 favicon.ico 是浏览器页签内部…

尚融宝——整合OpenFeign与Sentinel实现兜底方法——验证手机号码是否注册功能

一、整合过程 在项目添加依赖&#xff1a;添加位置 <!--服务调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency> 在需要的服务中添加启动注…

spring中常见的注解

DI(依赖注入中常见的注解) Autowired&#xff1a;按类型自动装配Resource&#xff1a;按名称或类型自动装配&#xff0c;Qualifier&#xff1a;按名称自动装配&#xff0c;Value &#xff1a;注入int、float、String等基本数据类型&#xff0c;只能标注在成员变量、setter方法上…

【Gradle-1】入门Gradle,前置必读

1、为什么要学习Gradle Gradle作为Android开发默认的构建工具&#xff0c;你的每一次编译都会用到它。招聘要求从以前的熟悉加分&#xff0c;到现在的必备技能&#xff0c;可见Gradle的重要性。 做开发这么久了&#xff0c;你是否对Gradle又爱又恨&#xff1f;是否对Gradle的…

第三章(1):自然语言处理概述:应用、历史和未来

第三章&#xff08;1&#xff09;&#xff1a;自然语言处理概述&#xff1a;应用、历史和未来 目录第三章&#xff08;1&#xff09;&#xff1a;自然语言处理概述&#xff1a;应用、历史和未来1. 自然语言处理概述&#xff1a;应用、历史和未来1.1 主要应用1.2 历史1.3 NLP的新…

【科普】PCB为什么常用50Ω阻抗?6大原因

在PCB设计中&#xff0c;阻抗通常是指传输线的特性阻抗&#xff0c;这是电磁波在导线中传输时的特性阻抗&#xff0c;与导线的几何形状、介质材料和导线周围环境等因素有关。 对于一般的高速数字信号传输和RF电路&#xff0c;50Ω是一个常用的阻抗值。 为什么是50Ω&#xff1f…

《程序员面试金典(第6版)》面试题 10.09. 排序矩阵查找(观察法,二分法,分治算法入门题目,C++)

题目描述 给定MN矩阵&#xff0c;每一行、每一列都按升序排列&#xff0c;请编写代码找出某元素。 示例: 现有矩阵 matrix 如下&#xff1a;[[1, 4, 7, 11, 15],[2, 5, 8, 12, 19],[3, 6, 9, 16, 22],[10, 13, 14, 17, 24],[18, 21, 23, 26, 30] ]给定 target 5&…