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

news2025/9/20 8:33:16

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.coloradmin.cn/o/1682065.html

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

相关文章

(规格参考)ADP5360ACBZ-1-R7 电量计 电池管理IC,ADP5072ACBZ 双通道直流开关稳压器,ADL5903ACPZN 射频检测器

1、ADP5360ACBZ-1-R7&#xff1a;具有超低功耗电量计、电池保护功能的先进电池管理PMIC 功能&#xff1a;电池保护 电池化学成份&#xff1a;锂离子/聚合物 电池数&#xff1a;1 故障保护&#xff1a;超温&#xff0c;过压 接口&#xff1a;I2C 工作温度&#xff1a;-40C ~ 85…

安全且高效数据传输技术:Filelink跨网文件传输

Filelink跨网文件传输是一种高效、便捷的数据传输技术&#xff0c;它打破了传统文件传输方式的局限&#xff0c;实现了跨网络、跨平台的无缝传输。在企业跨网文件传输&#xff0c;Filelink都以其卓越的性能和稳定性赢得了广泛的认可。 在以往的文件传输过程中&#xff0c;我们常…

分享如何通过定时任务调用lighthouse前端测试脚本+在持续集成测试中调用lighthouse前端测试脚本

最近写了个小工具来优化lighthouse在实际工作中的使用&#xff0c;具体实现了&#xff1a;通过定时任务调用前端测试脚本在持续集成测试中调用前端测试脚本。由于在公司中已经应用&#xff0c;所以就不能提供源码了&#xff0c;这里简单说一下实现思路&#xff0c;希望可以帮助…

数仓架构之为什么要进行数仓分层

数仓分层这个概念想必大家都很熟悉&#xff0c;不管是在实际的开发工作当中会用的&#xff0c;还是在面试官面试你的时候会问到&#xff1a;你之前的项目是按照什么分层的&#xff0c;分哪几层&#xff0c;数仓分层有什么好处&#xff0c;举个栗子说说。 简而言之&#xff0c;…

Metasploit基本命令

1. 开启控制台 命令&#xff1a; msfconsole2. 搜索模块 命令&#xff1a; search ms17-010 # 模块名这里以搜索 ms17-010 为例&#xff0c; auxiliary 开头的为测试模块&#xff0c;也就是 POC&#xff0c;看看存不存在漏洞&#xff0c; exploit 开头的为攻击模块 3. 调…

数据结构(十六)----外部排序

目录 一.外部排序 1.外部排序的原理 2.外部排序时间开销的分析 3.外部排序的优化 &#xff08;1&#xff09;多路归并 &#xff08;2&#xff09;减少初始归并段数量 二.败者树 三.置换-选择排序 四.最佳归并树 一.外部排序 1.外部排序的原理 若想清楚外部排序的原理…

Java GUI-登录注册功能实现

Java GUI-登录注册功能实现 技术栈&#xff1a; MySQL8.0JFrameSwing 功能描述&#xff1a; 登录&#xff1a;输入用户名、密码点击登录调转到登录页面注册&#xff1a;点击注册按钮&#xff0c;输入用户名和密码注册成功并返回注册页面注&#xff1a;本项目登录注册没有实现…

高考志愿系统-模拟填报模块分析

1.获取所有志愿列表 接口: http://localhost:81/dev-api/college_entrance/aspiration/list 默认传参pageNum1&pageSize10&#xff0c; 请求方法: GET 接口内方法同样首先设置分页信息&#xff0c;然后修改查询出的所有志愿信息列表中的学生id属性 2.详细志愿查看 接口…

YOLOv5改进 | Neck | 添加双向特征金字塔BiFPN【小白轻松上手 | 论文必备】

&#x1f680;&#x1f680;&#x1f680;本专栏所有的改进均可成功执行&#x1f680;&#x1f680;&#x1f680; 尽管Ultralytics 推出了最新版本的 YOLOv8 模型。但YOLOv5作为一个anchor base的目标检测的算法&#xff0c;YOLOv5可能比YOLOv8的效果更好。但是针对不同的数据…

cypress的安装使用

cypress npm install -g cnpm --registryhttps://registry.npm.taobao.org cypress的启动打开 npx cypress open js函数的回调 function print(string,callback){console.log(string)callback() } print("a",function(){print("b",function(){console.l…

STL <string>--------String的OJ题目

1.题目截图&#xff08;把字符串转换成整数----atoi&#xff09; 1.1题目解析&#xff08;在代码里&#xff09; class Solution { public:int myAtoi(string str) {// 100% 97.45% int len str.size();if(len 0)return 0;int i 0, flag 1, isSignal 0, res 0;while(…

QJsonObject构建指定的JSON结构

如今我们生活处处用到AI,AI 带给了我们很多方便&#xff0c;但作为程序员我们&#xff0c;虽然不能开发什么 AI&#xff0c;但时不时需要调用国内四大平台的AI接口。很多平台接口都是用JSON作为数据载体传送。 如下接口数据 &#xff0c;有些人不知道怎么构建。 1&#xff0c;…

[C++核心编程-08]----C++类和对象之运算符重载

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

黑马guli商城项目初始化-SpringCloud微服务项目初始化使用SpringCloudAlibaba快速搭建分布式系统

视频教程&#xff1a;https://www.bilibili.com/video/BV1np4y1C7Yf?p4&spm_id_frompageDriver&vd_source0b3904471b2f8a3a133e05bd42f729a9 这里写目录标题 1.服务架构图2.初始化目录结构3.初始化数据库4.使用逆向工程项目生成数据库CRUD5.创建工具项目6.配置mybati…

CentOS7使用Docker安装Redis图文教程

1.拉取Redis镜像 这里制定了版本&#xff0c;不指定默认latest最新版 docker pull redis:6.0.8提示信息如下即为下载成功 2.上传配置文件 官方配置文件&#xff08;找自己对应的版本&#xff09;&#xff1a;reids.conf 或者将如下配置文件命名为redis.conf&#xff0c;上…

面试题草稿

目录 一&#xff0e;JAVA基础 1.八个基本数据类型&#xff0c;长&#xff0c;占几个字节&#xff0c;取值范围是多少。 基本类型&#xff1a; 2.面向对象的特征 1. 封装&#xff08;Encapsulation&#xff09; 3.实现多态的几种方式 4.什么叫装箱什么叫拆箱 5.装拆箱分别…

Nginx启动关闭重启用脚本实现

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 Nginx(“engine x”…

快手截流多功能协议引流多线程多账号使用

在市场上&#xff0c;类似的软件售价都在几千元&#xff0c;但我发现这款全新版本的软件已经更新&#xff0c;而且我只需要配合使用谷歌浏览器&#xff0c;稍微调慢一点延时&#xff0c;我就可以像专业人士一样流畅地进行操作。 评论对于我而言是一种艺术&#xff0c;而不仅仅是…

植物大战僵尸杂交版(含下载方式)

最近时间&#xff0c;一款很火的植物大战僵尸杂交版火爆出圈&#xff0c;在玩家之间疯狂扩散。各种奇特的杂交组合让游戏变得更加有趣。 游戏介绍 植物大战僵尸杂交版是一款将《植物大战僵尸》和植物杂交概念结合在一起的独特塔防策略游戏。它将《植物大战僵尸》中的植物与进行…

【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置

本节博客主要是通过“在排序数组中查找元素的第一个和最后一个位置”总结关于二分算法的左右界代码模板&#xff0c;有需要借鉴即可。 目录 1.题目2.二分边界算法2.1查找区间左端点2.1.1循环条件2.1.2求中点的操作2.1.3总结 2.2查找区间右端点2.1.1循环条件2.1.2求中点的操作2.…