DAY45:动态规划(六)背包问题优化:一维DP解决01背包问题

news2024/11/24 22:43:03

文章目录

      • 一维DP数组的解法
        • 二维DP递推思路
        • 滚动数组优化思路(重要)
        • 一维DP数组的含义
        • 一维DP递推公式
        • 一维DP的初始化
        • 遍历顺序(重要)
        • 举例推导DP数组
      • 一维DP数组完整版写法
      • 面试问题
        • 为什么一维DP背包的for循环一定要倒序遍历
        • 为什么一维DP的for循环遍历,必须先遍历物品再遍历背包

一维DP数组的解法

背包最大重量为4。

物品重量和价值为:

在这里插入图片描述
问背包能背的物品最大价值是多少?

在我们使用二维DP数组的时候,递推公式是dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]).

如果要降为一维DP数组,就是用dp[j]来表示递推。这里用j是为了j的含义和二维DP数组保持一致,下标含义都是背包的容量

二维DP递推思路

原始的二维DP状态转移方程是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

这个方程意味着,对于第i个物品和当前背包容量j,我们要么选择放入这个物品,要么不放。如果放入这个物品,就需要看在容量为j-weight[i]时,放入前i-1个物品的最大价值(也就是dp[i - 1][j - weight[i]] + value[i]),如果不放入这个物品,就是dp[i - 1][j]。然后取这两者之间的最大值。

二维背包DP数组情况示例如下图所示。

在这里插入图片描述
我们其实可以发现,如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

滚动数组优化思路(重要)

leetcode题目343.整数拆分 里,其实有类似滚动数组的思想。整数拆分题目代码:

class Solution {
public:
    int integerBreak(int n) {
        //DP数组建立,注意数组本身容量赋值
        vector<int>dp(n+1,0);
        //初始化
        dp[2]=1;
        for(int i=3;i<=n;i++){
            for(int j=1;j<=i-1;j++){
                dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }
};

对于题目"整数拆分"来说,状态转移方程可以理解为:

dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))

其中,dp[i]表示整数i能得到的最大乘积,j * (i - j)表示将i拆分为j和i-j两个数的乘积,j * dp[i - j]表示将i拆分为j和另一个数,另一个数可以继续拆分得到的最大乘积。

这里**dp[i]是通过遍历j来不断更新得到的,也就是说,dp[i]会在过程中不断地被自己更新,这种自我更新的思想就类似于滚动数组的思想**。

"滚动数组"是一种优化动态规划中空间复杂度的技术。它的主要思想是仅保留DP过程中需要的几个状态而不是所有的状态,这样可以大大降低空间复杂度。

具体到背包问题,我们看到状态转移方程dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])dp[i][j]仅仅依赖于上一层(i-1)的状态。这意味着在计算当前层状态时,我们实际上并不需要保留所有以前的状态,只需要保留上一层的状态即可。这就为使用滚动数组提供了可能

那么,为何可以将dp[i - 1]这一层拷贝到dp[i]上,或者说使用一维数组代替二维数组?

我们可以将二维的dp数组理解为一个状态表格,其中行代表物品,列代表背包容量。如下图所示。

在这里插入图片描述
我们需要填满这个表格,每一个dp[i][j]都是由dp[i-1][j]或者dp[i-1][j-weight[i]]+value[i]转移过来的,也就是说**dp[i][j]只依赖于上一行(i-1)的状态。因此,我们实际上并不需要记住所有的行,只需要记住最后一行(也就是最新的一行)的状态就足够了**。

所以,我们将二维数组降维到一维数组,就是将dp[i][j]简化为dp[j]。在每一次迭代时,dp[j]在“滚动更新”dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),这个过程就像一个滚轮一样,不断地推进,并在这个过程中更新状态。因此,称之为"滚动数组"。

一维DP数组的含义

如果用一维数组优化这个问题,那么一维DP数组的含义是:dp[j]表示当前背包容量为j时的最大价值

我们遍历所有的物品,对于当前的物品,如果我们选择放入,那么dp[j]就需要更新为dp[j - weight[i]] + value[i],如果不放入,dp[j]就保持不变。然后我们取这两者之间的最大值,这样dp[j]就始终表示当前背包容量为j时的最大价值

一维DP递推公式

一维DP的递推公式为:

dp[j]=max(dp[j],dp[j-weight[i]+value[i]]);//不放入i的情况,和放入i的情况

其中dp[j]表示容量为j时的最大价值,dp[j-weight[i]]+value[i]表示当放入特定物品i时的背包最大价值。递推公式遍历情况如下图所示:

在这里插入图片描述
一维背包DP的递推思路就是,每遍历一个新的物品i,都从头遍历背包的所有容量j,得到每i对应的dp[j]数组,并且不断更新历史最大值确保dp[j]是容量为j时,背包的最大价值。(相当于压缩了二维DP数组中的i,只剩下了每个i对应的DP[j],并且不断更新确保dp[j]最大。)

这就是滚动数组的思想:尽管我们在遍历的过程中,dp[j]的值会不断变化,但是每一次变化后,dp[j]都会保存当前为止遍历到的最大值。这样,在遍历完所有物品后,dp[j]就是我们的答案,即背包容量为j时的最大价值。这个过程中,dp[j]不断自我更新,就是滚动数组的关键所在

一维DP的初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

实际上一维背包问题直接全部初始化为0即可,因为不存在数组下标越界问题,且j<weight[i]的情况本来就是单独判断。

因为dp数组在推导的时要取价值最大的数,因此其余下标都初始化为0。

遍历顺序(重要)

一维背包问题的遍历顺序模板如下:

//最外层是物品,物品个数是weight.size()
for(i=0;i<weight.size();i++){
    //里层是背包容量,注意背包必须倒序遍历
    for(int j=bagWeight,j>=weight[i];j--){
        递推公式;
    }
}

这个遍历顺序需要注意两点:

  • for的嵌套关系,最外层必须是物品,内层是背包,顺序不可颠倒
  • 内层for循环遍历背包的时候,需要倒着遍历,不能正序

关于for内外嵌套和内层遍历顺序为什么不能改变的问题,在面试问题中进行了整理。

举例推导DP数组

一维DP,分别使用物品0,物品1和物品2来遍历背包所有容量,使得**dp[j]满足dp[j]是容量为j的最大价值**。

得到的DP数组结果如下:

在这里插入图片描述
最后我们得到的结果,就是容量为4(背包最大容量就是4),且遍历完了最后一个物品的时候,dp[4]的数值。

结果在遍历完最后一个物品之后的原因是需要考虑所有备选的物品,这里需要结合dp[j]的含义进行理解。

一维DP数组完整版写法

  • 题目描述:背包容量4,物品0{重量1,价值15},物品1{重量3,价值20},物品2{重量4,价值30},求背包能装的最大价值
  • 和前文二维DP数组的写法用了同一模板和样例,方便对比
#include <bits/stdc++.h>
using namespace std;


int knapsack(vector<int>& weight, vector<int>& value, int bagWeight) {
    int n=weight.size();//物品数量
    //初始化
    vector<int>dp(n+1,0);
    for(int i=0;i<n;i++){//遍历物品
        for(int j=bagWeight,j>weight[i];j--){//倒序遍历背包
            dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
        }
    }
    return dp[bagWeight];
}

void test_knapsack() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    cout << knapsack(weight, value, bagWeight) << endl;
}

int main() {
    test_knapsack();
}

一维DP的写法比二维DP写法简洁很多,且空间复杂度降了一个数量级。

因此遇到背包类问题,最好使用一维DP写法。

面试问题

面试有可能的情况,是要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。

然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么?

为什么一维DP背包的for循环一定要倒序遍历

如果内层背包的for循环正序遍历的话,会出现物品被重复放置的情况。由于01背包每个物品只有一个,所以for循环必须倒序遍历,才能保证i的数值不变的情况下(也就是只有这一个物品的情况下),dp[j-weight[i]]不会被放进去很多次。

一旦正序遍历,例如i=0(物品0)的情况,物品0就会被放进去很多次

例如下图所示的情况:

在这里插入图片描述
背包容量正序遍历的情况,对应的是完全背包的情况,在完全背包情况下,每个物品有无限个,因此物品自身的容量可以进行叠加

而像上图的正序遍历情况,dp[2]的时候叠加了dp[1]的数值,而dp[1]的情况是放入了一个物品0。也就是说,dp[2]的情况是放入了两个物品0。不满足01背包问题每个物品只能有一个的要求。

为什么一维DP的for循环遍历,必须先遍历物品再遍历背包

从DP数组的含义来说:

物品的遍历顺序则必须在外层,这是因为我们需要对每一件物品进行处理,也就是每一件物品都需要有对应的DP[j]数组。dp[j]的含义是,考虑0–i所有物品情况下,背包的最大价值。因此每次处理一件新的物品时,我们都需要用到前面物品的信息(即dp数组),也就是说必须在处理新的物品前,就已经处理完前面的所有物品。因此,我们需要在外层循环中遍历物品。

如果将这两层循环的顺序进行调换,那么处理背包容量为j的情况时,很可能还没有处理完所有的物品,因此**dp[j]中存储的并不是在考虑了所有前面的物品后,背包容量为j时能取得的最大价值**,这与DP数组的含义不符合。

从背包放入物品的角度来说

因为一维dp的写法,背包容量一定是要倒序遍历(原因在上面)。而如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

我们还是以背包容量为4,物品有0 1 2三个并且分别有不同的value和weight的例子来举例。

一维DP背包问题是遍历所有的物品i,每个i都更新dp[j]的数组,使得dp[j]的值是容量为j且考虑0-i所有物品在内的最大价值

例如下图所示,是物品在外的情况下dp[j]数组的例子。可以看出,遍历到i=1的时刻,dp[j]数组已经继承了i=0的所有状态

在这里插入图片描述
但是,如果物品在内层for循环,背包容量在最外层,dp[j]的情况如下图所示:

(背包里只放入了一个物品)

在这里插入图片描述
从这个例子中我们可以看到,每次处理一件新的物品时,都需要用到前面物品的信息。只有当我们已经处理完所有的前面的物品,以及背包容量较大的情况,我们才能正确地处理当前的物品和背包容量。因此物品的for循环必须在外层,而背包容量的for循环必须在内层,且背包必须从大到小进行遍历。

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

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

相关文章

Qt + QR-Code-generator 生成二维码

0.前言 之前使用 libgrencode 生成二维码&#xff0c;LGPL 协议实在不方便&#xff0c;所以需要找一个 github 星星多的&#xff0c;代码简单最好 header-only&#xff0c;协议最好是 MIT 或者兼容协议而不是 GPL 或者 LPGL。 QR-Code-generator 正好符合这个要求&#xff0c…

JMeter 如何模拟不同的网络速度

目录 前言&#xff1a; 限制输出带宽以模拟不同的网络速度 将这两行添加到user.properties文件中&#xff08;可以在JMeter安装的bin文件夹中找到此行&#xff09; 通过-J 命令行参数传递属性的值&#xff0c;如下所示&#xff1a; 前言&#xff1a; JMeter可以通过使用不同…

【网络安全带你练爬虫-100练】第12练:pyquery解析库提取指定数据

目录 一、目标1、基础/环境的准备工作 二、目标2&#xff1a;开始使用pyquery 三、目标3&#xff1a;提取到指定的数据 四、目标3&#xff1a;通过列表的形式获取指定数据 五、扩展&#xff1a;其他方法 六、网络安全O 一、目标1、基础/环境的准备工作 1、文档&#xff1…

wordpress的wp_trim_words摘要没有截取到指定的字符长度的问题解决

一、问题描述 在imqd.cn文章列表中&#xff0c;用于将正文内容截取保留前75个字的方法突然没效果了。 <p class"post-desc text-black-50 d-none d-md-block"><?phpecho wp_trim_words( get_the_content(), 75, ...);?> </p>直接展示了所有的正…

计算机网络实验(1)--Windows网络测试工具

&#x1f4cd;实验目的 理解知识点&#xff08;ping&#xff0c;netstat&#xff0c;ipconfig&#xff0c;arp&#xff0c;tracert&#xff0c;route&#xff0c;nbtstat&#xff0c;net&#xff09;所涉及的基本概念&#xff0c;并学会使用这些工具测试网络的状态及从网上获取…

java后端开发环境搭建 mac

在mac pro上搭建一套java 后端开发环境&#xff0c;主要安装的内容有&#xff1a;jdk、maven、git、tomcat、mysql、navicat、IntelliJ、redis。 本人mac pro的系统为mac OS Monterey 12.6.7&#xff0c;主机的硬件架构为x86_64。 左上角关于本机查看系统版本&#xff1b;终端…

LLM - Baichuan7B Lora 训练详解

目录 一.引言 二.环境准备 三.模型训练 1.依赖引入与 tokenizer 加载 2.加载 DataSet 与 Model 3.Model 参数配置 4.获取 peft Model 5.构造 Trainer 训练 6.训练完整代码 四.Shell 执行 1.脚本构建 2.训练流程 3.训练结果 五.总结 一.引言 LLM - Baichuan7B Tok…

Ubuntu18.04 docker kafka 本地测试环境搭建

文章目录 一、kafka 介绍二、Ubuntu docker kafka 本地测试环境搭建2.1 docker kafka 启动2.1.1 下载镜像2.1.2 启动 wurstmeister/zookeeper2.1.3 启动 wurstmeister/kafka 2.2 docker kafka 测试数据收发2.2.1 docker kafka 测试数据收发2.2.2 windows验证 三、嵌入式集成四、…

加密劫持者攻击教育机构

我们的专家分析了2023年第一季度的当前网络威胁。研究表明&#xff0c;独特事件的数量增加&#xff0c;勒索软件活动激增&#xff0c;特别是针对学术和教育机构。我们记录了大量与就业有关的网络钓鱼邮件&#xff0c;出现了QR网络钓鱼和恶意广告的增加。 我们的研究表明&#…

「2023 最新版」Java 工程师面试题总结 (1000 道题含答案解析)

作为一名优秀的程序员&#xff0c;技术面试都是不可避免的一个环节&#xff0c;一般技术面试官都会通过自己的方式去考察程序员的技术功底与基础理论知识。 如果你参加过一些大厂面试&#xff0c;肯定会遇到一些这样的问题&#xff1a; 1、看你项目都用的框架&#xff0c;熟悉…

AIGC:文生图stable-diffusion-webui部署及使用

1 stable-diffusion-webui介绍 Stable Diffusion Web UI 是一个基于 Stable Diffusion 的基础应用&#xff0c;利用 gradio 模块搭建出交互程序&#xff0c;可以在低代码 GUI 中立即访问 Stable Diffusion Stable Diffusion 是一个画像生成 AI&#xff0c;能够模拟和重建几乎…

Linux(centos7)下安装mariadb10详解

MariaDB 和 MySQL 之间存在紧密的关系。 起源&#xff1a;MariaDB 最初是作为 MySQL 的一个分支而创建的。它的初始目标是保持与 MySQL 的兼容性&#xff0c;并提供额外的功能和性能改进。 共同的代码基础&#xff1a;MariaDB 使用了 MySQL 的代码基础&#xff0c;并在此基础上…

PYTHON 解码 IP 层

PYTHON 解码 IP 层 引言1.编写流量嗅探器1.1 Windows 和 Linux 上的包嗅探2.解码 IP 层2.1 struct 库3.编写 IP 解码器4.解码 ICMP5.总结 作者&#xff1a;高玉涵 时间&#xff1a;2023.7.12 环境&#xff1a;Windows 10 专业版 22H2&#xff0c;Python 3.10.4 引言 IP 是 …

JWT的深入理解

1、JWT是什么 JWT&#xff08;JSON Web Token&#xff09;是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在不同实体之间安全地传输信息。它由三部分组成&#xff0c;即头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&…

获取QT界面坐标的各种方法

链接 ract() 获取rect所在部件的尺寸。 rect()返回的QRect对象可以用来做什么

openResty的Redis模块踩坑记录

OpenResty提供了操作Redis的模块&#xff0c;我们只要引入该模块就能直接使用。说是这样说&#xff0c;但是实践起来好像并不太顺利。 1.设置了密码的redis&#xff0c;lua业务逻辑中需要添加身份认证代码 网上很多资料、文章似乎都是没有设置redis密码&#xff0c;说来也奇怪…

JS区域滤镜

思路 简单一点的&#xff0c;像素点X坐标小于图宽1/3和大于2/3的点变灰&#xff0c;中间的点不变。 复杂的暂时不会搞。 原图 处理后 <html> <style> #canvas { width:100%; } </style> <body> <input id"file" type"file" …

python中的生成器(generator)

一、生成器 生成器是 Python 中非常有用的一种数据类型&#xff0c;它可以让你在 Python 中更加高效地处理大量数据。生成器可以让你一次生成一个值&#xff0c;而不是一次生成一个序列&#xff0c;这样可以节省内存并提高性能 二、实现generator的两种方式 python中的gener…

SAP从放弃到入门系列之WIP Batch(Work-in-Process ) -Part1

目录 一、 概述二、 系统配置三、 数据设置最后 ERP系统的复杂性并不单是架构设计和技术造成的&#xff0c;而是它所要支撑的业务场景&#xff0c;涉及行业越广泛越复杂软件功能越复杂&#xff0c;复杂的背后是业务实践沉淀和优化的流程。平时看着部分系统功能很复杂&#xff0…

47.判断类关键字 if else switch case default

目录 1 if 2 else 3 判断的嵌套 4 switch,case,default 4.1 基本使用 4.2 需要注意的点 1 if if后面的括号加表达式的内容&#xff0c;大括号中加入 条件为true 时要运行的代码 经测试如果我们将a的值设置为0&#xff0c;则不会弹出警告框 2 else 和if配合使用…