详解动态规划之01背包问题及其空间压缩(图文并茂+例题讲解)

1. 动态规划问题的本质

  • 记忆化地暴力搜索所有可能性来得到问题的解

我们常常会遇到一些问题,需要我们在n次操作,且每次操作有k种选择时,求出最终需要的最小或最大代价。处理类似的问题,我们一般需要遍历所有的可能性(相当于走一遍所有的路径),然后找到我们所需要的解。
很明显我们可以构成一棵“决策树”,假设n=2,k=3,那么:

在这里插入图片描述

我们可以通过DFS或者BFS来遍历整棵树,从而搜寻到我们需要的结果。
时间复杂度:O(k^n)

但是我们可以看到,时间复杂度是指数级别的,一旦n的数级够大,那么时间代价是极大的。

这个时候,我们就需要用到动态规划的思想了。我们可以发现,执行决策的时候,经常会出现重复决策的地方,在第一次操作的时候有三种决策,在第二次操作时,在执行了决策1的情况下,又执行了三次相同的决策。这些重复的路径所导致的结果是相同的,那么我们完全可以直接调用之前的结果,大大减少了时间代价。
那么我们是否可以将遍历过的路径保存在一个数组(容器)中,如果我们遇到了重复的路径,直接访问之前访问过的路径的结果,并将其转移过来到当前的位置。

所以其本质上还是暴力搜索了所有可能,只不过加上了记忆曾经遍历过的路径

2. 01背包问题

01背包问题是主要解决n个物品的放置问题,对于物品的放与不放,如果放的话改怎么放的决策进行搜索。
下面会根据一道蓝桥杯的典型例题来辅助讲解01背包及其空间压缩的问题。

题目链接:

https://www.luogu.com.cn/problem/P8742

题面描述

在这里插入图片描述

思路分析

  • 显而易见,每个砝码的放置有三种情况:1.放左边 2.放右边 3.不放。如果使用DFS遍历所有情况,那么时间复杂度为3^n,n的取值可以到100,必然超时。
  • 假设当我们放完第一个物品时,我们开始放第二个物品,我们可以单独放;可以放在第一个物品的同侧,也可以放在第一个物品的异侧。后两种情况是建立在第一个物品放的情况下结果,所以我们很明显要知道是否存在物品一放了的重量是否存在。
  • 我们定义dp[i][j]的含义是之考虑前i个物品放置的所有情况下,背包容量为j时,是否存在
    这样的物品放置的情况刚好装满。
  • 则状态转移方程为:
    1.当不放第i个物品时: dp[i][j]=dp[i-1][j]
    2.当只有第i个物品,即如果j==a[i]dp[i][j]=1
    3.当减少第i个物品,即如果dp[i-1][j+a[i]]==1dp[i][j]=1
    4.当增加第第i个物品,即如果dp[i-1][|j-a[i]|==1dp[i][j]=1
  • 我们来分析一下状态转移方程:
    因为我们有三种决策:1.放左边 2.放右边 3.不放。
  1. 不放物品就不必说了,直接转移保存上次的状态即可。其次,我们可以单独放一个物品,所以有了方程2;然后是在放前i-1的物品的情况下,将第i个物品放在左侧或右侧。
  2. 假设我们以左侧的重量来遍历j,那么放在左侧就是加上这个砝码,那么就需要记忆化查询没有加上这个砝码时的重量是否存在,若存在,那么j可以称出,否则不能;放在右侧也是同理。

下面是AC代码

#include "bits/stdc++.h"
using namespace std;
#define int long long
#define endl '\n'
const int MAXN=105;
const int MAXM=1e5+10;
int n;
int a[MAXN];
int dp[MAXN][MAXM];
void solve() {cin >> n;int s=0;for (int i = 1; i <= n; ++i) {cin >> a[i];s+=a[i];}dp[0][0] = 1;for (int i = 1; i <= n; ++i) {for (int j = 0; j <= s; ++j) {dp[i][j] = dp[i-1][j];if (j == a[i]) dp[i][j] = 1;if (j+a[i] <= s && dp[i-1][j+a[i]] == 1) dp[i][j] = 1;if (dp[i-1][abs(j-a[i])] == 1) dp[i][j] = 1;}}int ans=0;for (int j = 1; j <= s; ++j) {if (dp[n][j] == 1) ans++;}cout << ans << endl;
}signed main() {ios :: sync_with_stdio(false);cin.tie(nullptr);int t=1;//cin >> t;while (t--) solve();
}

3. 如何进行空间压缩

我们可以发现,每次遍历到第i个砝码的时候,我们只会用到上一次(第i-1)时的状态,从0~i-2的空间不再被使用,那么我们完全可以将其压缩掉,达到优化空间复杂度的目的。

那么该如何进行空间压缩呢?

这里我们引入了滚动数组的概念,将dp数组想象成动态滚动更新的。可以视作将数组分为两部分:一部分是i-1状态下的数组;另一部分是i状态下的数组:
在这里插入图片描述
我们在增加第i个物品的时候,需要找到未考虑第i个物品时(i-1状态)且未增加第i个物品的重量(j-a[i])这个时候的状态,然后考虑进行状态转移。
假设如上图所示,我们现在遍历到重量为6的状态,那么6-a[i]的质量6的前面(保证a[i]范围的合理性),而6的前面刚好都是i-1状态下的,并没有被更新。就这样一直滚动,知道数组全部被更新为i状态。
这也是为什么背包问题被压缩为一维后,需要逆序遍历背包容量的原因。那么同理,如果需要减少第i个物品,那么我们要拿到i-1下的状态,那就要顺序遍历

压缩后的代码

#include "bits/stdc++.h"
using namespace std;
#define int long long
#define endl '\n'
const int MAXN=105;
const int MAXM=1e5+10;
int n;
int a[MAXN];
int dp[MAXM];
void solve() {cin >> n;int s=0;for (int i = 1; i <= n; ++i) {cin >> a[i];s+=a[i];}dp[0] = 1;for (int i = 1; i <= n; ++i) {for (int j = s; j >= a[i]; --j) {if (dp[j-a[i]] == 1) dp[j] = 1;}}for (int i = 1; i <= n; ++i) {for (int j = 1; j <= s-a[i]; ++j) {if (dp[j+a[i]] == 1) dp[j] = 1;}}int ans=0;for (int j = 1; j <= s; ++j) {if (dp[j] == 1) ans++;}cout << ans << endl;
}signed main() {ios :: sync_with_stdio(false);cin.tie(nullptr);int t=1;//cin >> t;while (t--) solve();
}

总结

  • 总而言之,言而总之。动态规划问题的核心是利用空间换取时间,将曾经经过的状态保存下来,当在往后的遍历中如果需要该状态就不必再次重复一遍,只需直接访问当时的状态结果即可。
  • 而滚动数组的用法很巧妙地将上一层地状态保存下来的同时完成了对当前状态的更新。
    希望此文章会对您对动态规划的学习有一定帮助~

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32-串口通信波特率计算以及寄存器的配置详解

您好&#xff0c;我们一些喜欢嵌入式的朋友一起建立的一个技术交流平台&#xff0c;本着大家一起互相学习的心态而建立&#xff0c;不太成熟&#xff0c;希望志同道合的朋友一起来&#xff0c;抱歉打扰您了QQ群372991598 串口通信基本原理 处理器与外部设备通信的两种方式 并行…

邮箱地址验证软件有哪些-邮件地址验证软件

邮箱地址验证软件是帮助用户验证电子邮箱地址是否有效和真实存在的工具。以下是一些常用的邮箱地址验证软件&#xff1a; 易邮件地址验证大师&#xff1a;这是电子邮件营销平台MailerLite提供的一个简单的电子邮件验证工具&#xff0c;通过多层验证过程保证高准确率。寅甲邮件…

ChatGPT-4o 实战 如何快速分析混淆加密和webpack打包的源码

ChatGPT-4o 几个特点 一个对话拥有长时间的记忆&#xff0c;可以连续上传文件&#xff0c;让其分析&#xff0c;最大一个代码文件只能3M&#xff0c;超出3M的文件&#xff0c;可以通过split-file可以进行拆分 其次ChatGPT-4o可以生成文件的下载链接&#xff0c;这有利于大文件的…

TypeScript的数据类型系统

TypeScript的数据类型系统 在上一篇文章中&#xff0c;我们介绍了TypeScript的基本概念和它与JavaScript的关系。TypeScript的核心优势之一是其强大的类型系统&#xff0c;它提供了丰富的数据类型&#xff0c;使得代码更加可靠和易于维护。本文将深入探讨TypeScript中的各种数…

gpt4o在哪用?

GPT-4o功能&#xff1f; 1.感知用户情绪&#xff1a;前沿研究部门主管陈信翰&#xff08;Mark Chen&#xff09;让ChatGPT-4o聆听他的呼吸&#xff0c;聊天机器人侦测到他急促的呼吸&#xff0c;并幽默地建议他不要像吸尘器那样呼吸&#xff0c;要放慢速度。随后Mark深呼吸一次…

浏览器插件Video Speed Controller(视频倍速播放),与网页自身快捷键冲突/重复/叠加的解决办法

浏览器插件Video Speed Controller&#xff08;视频倍速播放&#xff09;&#xff0c;与网站自身快捷键冲突/重复/叠加的解决办法 插件介绍问题曾今尝试的办法今日发现插件列表中打开Video Speed Controller的设置设置页面翻到下面&#xff0c;打开实验性功能。将需要屏蔽的原网…

邮件API接口的优势有哪些?如何有效整合?

邮件API怎么选&#xff1f;SendCloud与AokSend的性能对比分析&#xff1f; 邮件API接口作为企业与用户沟通的重要桥梁&#xff0c;其重要性不言而喻。Aok将深入探讨邮件API接口的优势、有效整合的方法、选择标准以及SendCloud与AokSend两款邮件发送服务的性能对比分析。 邮件…

杨校老师项目之基于SpringBoot+Shiro+Vue的企业人事管理系统

1.获取代码&#xff1a; 有偿获取&#xff1a;mryang511688 2.技术栈 后端 SpringBoot MySQL mybatis-plus shiro Redis 前端 Vue Element-UI 3.开发环境 JDK1.8、Maven3.5.4、MySQL5.7、Redis5.0.5、IntelliJ IDEA、nodejs 4.内置功能 Springboot的项目&#xff0c;…

Hive的窗口函数

定义&#xff1a; 聚合函数是针对定义的行集(组)执行聚集,每组只返回一个值.如sum()、avg()、max() 窗口函数也是针对定义的行集(组)执行聚集,可为每组返回多个值.如既要显示聚集前的数据,又要显示聚集后的数据.步骤&#xff1a; 1.将记录分割成多个分区. 2.在各个分区上调用窗…

工业派-配置Intel神经计算棒二代(NCS2)

最近两天在工业派ubuntu16.04上配置了Intel神经计算棒二代——Intel Neural Compute Stick&#xff0c;配置过程之艰辛我都不想说了&#xff0c;实在是太折磨人。不过历尽千辛万苦&#xff0c;总算让计算棒可以在工业派ubuntu16.04系统上跑了&#xff0c;还是蛮欣慰的。 注&…

究极完整版!!Centos6.9安装最适配的python和yum,附带教大家如何写Centos6.9的yum.repos.d配置文件。亲测可行!

前言&#xff01; 这里我真是要被Centos6.9给坑惨了&#xff0c;最刚开始学习linux的时候并没有在意那么的&#xff0c;没有考虑到选版本问题&#xff0c;直到23年下半年&#xff0c;官方不维护Centos6.9了&#xff0c;基本上当时配置的文件和安装的依赖都用不了了&#xff0c…

干式蒸发器、满液式蒸发器以及降膜式蒸发器的介绍

干式蒸发器 1、干式蒸发器原理、定义 干式蒸发器制冷剂在换热管内通过&#xff0c;冷水在高效换热管外运行&#xff0c;这样的换热器换热效率相对较低&#xff0c;其换热系数仅为光管换热系数的2倍左右&#xff0c;但是其优点是便于回油&#xff0c;控制较为简便&#xff0c;…