从二维数组到一维数组——探索01背包问题的动态规划优化


文章目录

  • 题目
  • 前知
    • 背包问题
  • 二维dp数组
    • 一、思路
    • 二、解题方法
    • 三、Code
  • 一维dp数组
    • 一、思路
    • 二、解题方法
    • 三、Code
  • 总结


本文将继续上一篇博客爬楼梯之后继续讲解同样用到了动态规划的 01背包问题

在解决动态规划问题时,我们经常面临着空间复杂度的挑战。01背包问题是一个典型的例子,通常使用二维数组来表示状态转移,但这样会带来额外的空间开销。在本文中,我们将探讨如何通过优化空间复杂度,将01背包问题从二维数组降维到一维数组,以提高算法的效率和性能。

题目

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

01背包重量价值
物品0115
物品1320
物品2430

前知

背包问题

01背包问题是一个经典的动态规划问题,通常用来解决如何在有限的背包容量下选择物品以获得最大价值的问题。问题的描述是:给定一组物品,每种物品都有自己的重量和价值,在限定的背包容量下,选择哪些物品放入背包可以使得背包内物品的总价值最大化,且不能超过背包的承重。背包问题有以下几种:
在这里插入图片描述

在这里插入图片描述


二维dp数组

一、思路

首先,我们使用动态规划来解决01背包问题。通常,我们会使用一个二维数组dp[i][j]来表示在前i个物品中,背包容量为j时的最大总价值。我们初始化dp数组为0,并通过状态转移方程来更新dp数组,最终得到dp[n][C]作为问题的解,其中n为物品个数,C为背包容量。

01背包重量价值
物品0115
物品1320
物品2430

二、解题方法

动规五部曲

  1. 确定dp数组及下标i的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是dp[i][j] ,如下图所示:
    在这里插入图片描述

  2. 确定递推公式:dp[i][j]可以由两个方向推出,要么是放i,要么就不放i,递推公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  • 不放物品i时:里面的最大价值实际上是dp[i - 1][j],因为此时代表已经达到背包最大容量时的最大价值,和上一层状态相同,当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。
  • 放物品i时:现在里面已经放了i,最大价值所以肯定有 value[i])dp[i - 1][j - weight[i]]代表的是放物品i并且剩余空间能够刚好放下物品i时的最大价值
  1. dp数组如何初始化:如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包里面都放不下物品,背包价值总和一定为0,如图:在这里插入图片描述
    再由递推公式可以看出dp[i][j]一定是由dp[i-1]得出的,所以还需要初始化最上面一层的dp数组,也就是dp[0][j]:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。当最小的背包容量j连一个物品都存不下的时候,那么就让dp[0][j]=0,例如题目中把物品0的重量改为2,dp[0][1]就为0。如果背包能放得下物品的时候,dp[0][j] 应该是value[0],并且都是所有容量的都为value[0],因为01背包问题,物品只能取一次,如图:
    在这里插入图片描述
for (int j = 0 ; j < weight[0]; j++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
    dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}

其它非0下标的dp数组都可以初始化为0,因为都是由上面一层或左上方推出来的,0会被覆盖掉
在这里插入图片描述

  1. 确定遍历顺序:从递推公式 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);可以看出,有两个for循环遍历方向得到dp数组,一个是物品,另一个是背包容量,由于dp[i][j]所需要的数据就是左上角,所以先遍历物品还是先遍历背包容量都没什么太大问题

先遍历物品,再遍历背包容量,一行一行遍历:
在这里插入图片描述
先遍历背包容量,再遍历物品,一列一列遍历:
在这里插入图片描述

  1. 举例推导dp数组:最终结果dp[2][4]
    在这里插入图片描述

三、Code

public class BagProblem {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
}

一维dp数组

一、思路

使用二维数组会带来额外的空间开销。为了优化空间复杂度,我们可以通过滚动数组将二维数组降维为一维数组。我们定义一个一维数组dp[j],其中dp[j]表示背包容量为j时的最大总价值。然后,我们先从前往后遍历物品,再通过逆序遍历背包容量dp数组来更新状态,最终得到dp[C]作为问题的解。这样,我们成功将空间复杂度从O(n*C)降低到了O(C),大大提高了算法的效率和性能。

01背包重量价值
物品0115
物品1320
物品2430

二、解题方法

把dp[i - 1]那一层拷贝到dp[i]上,对状态进行压缩,只用一个dp[j]滚动数组

动规五部曲

  1. 确定dp数组及下标i的含义:容量为j的背包,所背的物品价值可以最大为dp[j]

  2. 确定递推公式:一维dp数组相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,所以递推公式为dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  3. dp数组如何初始化:当背包容量为0时,所背的物品价值dp[0]最大就是0,dp数组非0下标都让值为1,这样在递推之后,取到的最大值就会是递推过程中的最大值,而不会被其它初始值给覆盖掉

  4. 确定遍历顺序:根据递推公式得出同样有两个方向,首先从前到后遍历物品,再倒序遍历背包容量,这个顺序不能像二维dp数组一样调换顺序。

  • 如果是先倒序遍历背包容量,再正序遍历物品的话,那么里面的for循环会重复从第一个物品开始遍历,就只会在背包里添加一个物品。
  • 如果是先从前到后遍历物品,再正序遍历背包容量的话,物品0就不止添加了一次,而会被重复添加,从后向前遍历的话,前面的状态就不会被后面所调用到。
    正序:dp[1] = dp[1 - weight[0]] + value[0] = 15
    dp[2] = dp[2 - weight[0]] + value[0] = 30
    倒序:dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
    dp[1] = dp[1 - weight[0]] + value[0] = 15
  • 二维dp数组遍历的时候不用倒序是因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}
  1. 举例推导dp数组:用题目进行举例
    在这里插入图片描述

三、Code

    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWight = 4;
        testWeightBagProblem(weight, value, bagWight);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
        int wLen = weight.length;
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagWeight + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < wLen; i++){
            for (int j = bagWeight; j >= weight[i]; j--){
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //打印dp数组
        for (int j = 0; j <= bagWeight; j++){
            System.out.print(dp[j] + " ");
        }
    }

总结

通过这篇博客,读者可以清晰地了解如何通过优化空间复杂度,将01背包问题的动态规划解法从二维数组降维到一维数组,并且可以对比二者在性能上的差异,从而更好地掌握这一知识点。希望本文能够帮助读者更好地理解和应用动态规划算法在01背包问题中的使用,如果有任何疑问或者建议,欢迎留言讨论🌹

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

书生·浦语大模型-第三节课笔记/作业

笔记 作业 原版 prompt控制节奏&#xff0c;实现类似关键词检索、主题、信息抽取等功能注意这里根据llm返回的topic (prompt: 告诉我这句话的主题&#xff0c;直接说主题不要解释)进行召回检索(CacheRetriever), 并再次让大模型判断query与返回的检索的相关程度. 如果本地检索…

【工具-工具指南】

项目-开发工具 ■ 编辑器■ Xmind ■ UI交互设计■ AxureRP9 ■ 项目管理■ boardmix■ excalidraw ■ Markdown■ MarkText■ Typora■ Ulysses■ Notable■ VNote■ Mou■ Bears■ Notion■ 有道云■ 印象笔记 ■ 硬件画图■ AD■ Allegro■ PADS■ Eagle■ Altium■ Fritzin…

保研线性代数复习4

一.范数&#xff08;Norms&#xff09; 1.什么是范数&#xff1f; 范数是一个向量空间V的函数&#xff0c;每一个属于向量空间V的向量x都匹配了一个实数&#xff08;它的长度&#xff09;&#xff1a; 2.范数的性质&#xff1f; 齐次性&#xff1a; 正定性&#xff1a; 三…

SpringBoot整合MyBatis四种常用的分页方式

目录 方式1 一、准备工作 1. 创建表结构 2. 导入表数据 3. 导入pom.xml依赖 4. 配置application.yml文件 5. 创建公用的实体类 项目结构 2. 创建controller层 3. 创建service层 4. 创建mapper层 5. 创建xml文件 6. 使用postman进行测试&#xff0c;测试结果如下…

第6章 6.1.1 文本格式化 sprintf函数(MATLAB入门课程)

sprintf函数源自 C 语言标准库中的同名函数&#xff0c;这个函数在 C 语言中用于创建格式化的字符串&#xff0c;且使用频率非常高。作为一门高级编程语言&#xff0c;MATLAB借鉴了 C 语言和其他编程语言中的许多特性和命名惯例。在MATLAB中&#xff0c;sprintf函数主要有两种用…

学习记录14-运算放大器2

目录 前言 一、理想放大器 二、虚断 二、虚短 虚短的两个使用条件 1.虚短概念 2.如果我们将运放的同相端和反相端颠倒会怎样呢&#xff1f; 总结 前言 主要讲述运算放大器的虚短虚断 一、理想放大器 如果没有基础或只是想简单了解&#xff0c;可以看我前一篇文章&am…

数学基础:常见函数图像

来自&#xff1a; https://www.desmos.com/calculator/l3u8133jwj?langzh-CN 推荐: https://www.shuxuele.com/index.html 一、三角函数 1.1 正弦 sin(x) 1.2 余弦 cos(x) 1.3 正切 tan(x) 1.4 余切 cot(x) 1.5 正弦余弦综合 1.6 正切余切综合 二、指数对数

【数据结构与算法】力扣 19. 删除链表的倒数第 N 个结点

题目描述 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a; head [1,2,3,4,5], n 2 输出&#xff1a; [1,2,3,5]示例 2&#xff1a; 输入&#xff1a; head [1], n 1 输出&#xff1a; []示例…

[方案实操|数据技术]数据要素十大创新模式(3):深数所-数据交易动态合规体系

“ 推动数据要素更好发展&#xff0c;政策创新是前提&#xff0c;数据质量管理是基础&#xff0c;安全和隐私保护是关键&#xff0c;合规性遵循是条件&#xff0c;数据共享和交易平台是手段。” 数据要素十大创新模式系列文章。 [方案实操|数据技术]数据要素十大创新模式(1)&a…

uniapp 表单使用Uview校验 包括城市选择器

<view><!-- 注意&#xff0c;如果需要兼容微信小程序&#xff0c;最好通过setRules方法设置rules规则 --><u--form labelPosition"left" :model"model1" :rules"rules" ref"uForm" labelWidth"174"><u…

生产端消息可靠性保证: 确认(Confirm)机制

1.PostConstruct注解 PostConstruct注解是Java EE规范中的一部分&#xff0c;主要用于标记在一个Bean初始化完成后需要执行的方法。这个注解由JSR-250定义&#xff0c;并且在Spring框架以及其他遵循Java EE标准的应用服务器中广泛支持。 功能与用途&#xff1a;初始化方法,当…

扫描IP开放端口该脚本用于对特定目标主机进行常见端口扫描(加载端口字典)或者指定端口扫描,判断目标主机开

扫描IP开放端口该脚本用于对特定目标主机进行常见端口扫描(加载端口字典)或者指定端口扫描,判断目标主机开 #/bin/bash #该脚本用于对特定目标主机进行常见端口扫描(加载端口字典)或者指定端口扫描,判断目标主机开放来哪些端口 #用telnet方式 IP$1 #IP119.254.3.28 #获得IP的前…

UML学习

UML(Unified Modeling Language)&#xff1a;统一建模语言&#xff0c;提供了一套符号和规则来帮助分析师和设计师表达系统的架构、行为和交互 类图&#xff1a;描绘类、接口之间的关系(继承、实现、关联、依赖等)以及类的内部结构(属性和方法)&#xff0c;直观展现系统的静态…

书生浦语大模型实战营第二次课作业

目录 教程来源1.环境配置1.1 初始化环境1.2 激活创建的虚拟环境然后继续安装所需的python包 2.下载模型并运行模型2.1 创建相应的文件夹和文件2.2 编写下载模型的代码运行下载模型2.3 编写运行模型的代码运行代码 测试模型效果 教程来源 视频教程&#xff1a;https://www.bili…

4套java开发的智慧系统源码 智慧校园系统源码 智慧工地系统源码 智慧城管系统源码

4套java智慧系统源码 智慧校园系统源码 智慧工地系统源码 智慧城管系统源码 3D 智能导诊系统源码 Java智慧校园系统源码 智慧学校源码 微信小程序电子班牌 智慧校园系统简介&#xff1a; 智慧校园的建设逐渐被师生、家长认可接受&#xff0c;智慧校园通过对在校师生、教务等…

IO-DAY8

使用消息队列去实现2个终端之间的互相聊天 要求:千万不要做出来2个终端之间的消息发送是读一写的&#xff0c;一定要能够做到&#xff0c;一个终端发送n条消息&#xff0c;另一个终端一条消息都不回复 A终端&#xff1a; #include<myhead.h> typedef struct msgbuf {lon…

蝙蝠优化算法(bat optimization algorithm)

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 算法背景 蝙蝠优化算法&#xff08;Bat Algorithm&#xff09;是一种基于群体智能的优化算法&#xff0c;它的灵感来源于蝙蝠捕食时的回声定位行…

LeetCode 377——组合总和 Ⅳ

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 此题一看应该就是需要用到动态规划算法&#xff0c;假设我们以 f[d]表示总和为 d 的元素组合的个数&#xff0c;首先&#xff0c;我们遍历 nums 数组&#xff0c; 如果有 nums[i] < target&#xff0c;那么组…

阿赵UE学习笔记——26、动画混合空间

阿赵UE学习笔记目录   大家好&#xff0c;我是阿赵。   继续学习虚幻引擎的使用。之前学习了通过蓝图直接控制动画播放&#xff0c;或者通过动画状态机去控制播放。这次来学习一种比较细致的动画控制播放方式&#xff0c;叫做动画混合空间。 一、使用的情景 假设我们现在需…

06_vim编辑器

为什么要使用vi和vim vi和vim是最常用的文本编辑工具&#xff0c;就像Windows上的笔记本一样。在linux中修改任何文件&#xff0c;不管是nginx配置还是系统配置文件&#xff0c;都会用到vi和vim命令。 很多软件的编辑接口实际上调用的是vi。 vim是vi的高级版&#xff0c;操作…