动态规划——背包问题(01背包、完全背包,分组背包与二进制优化)

本蒟蒻写二进制优化开始的时候写昏了,并且昏了一下午。但好在有神犇救命,这篇博客才得以面世——躲着人群

一、01背包

概述:

其常见的问题形式为:给出n个物品,每个物品有对应的价值和体积。给出背包容量后求不超过背包容量的条件下能获得物品的价值总和的最大/最小值 

实现:

定义一个二维数组,dp[i][j] 表示在考虑前 i 个物品,且背包容量为 j 的情况下,能够获得的最大价值。那么,我们就能得到如下的状态转移方程:

dp[i][j]=max(dp[i-1][j],dp[i][j-volume[i]]+value[i])

首先,对于每个物品,我们都有选和不选两种选择

        其中,dp[i-1][j]表示不选择第i个物品,dp[i][j-volume[i]]+value[i]表示选择第i个物品。这两者之中我们取值较大的那个。于是,我们就可以根据这个状态转移方程写出程序:

#include<bits/stdc++.h>
using namespace std;
int f[2000][2000],w[2000],p[2000],m,v;
int main(){cin>>v>>m;//m:物品数量 v:背包容积 for(int i=1;i<=m;i++){cin>>w[i]>>p[i];}for(int i=1;i<=m;i++){for(int j=1;j<=v;j++){if(w[i]<=j){f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+p[i]);//在 w[i]<=当前背包容量时(也就是可以选),来考虑选不选的问题 }else{f[i][j]=max(f[i][j],f[i-1][j]);//超出容积,选不了 }			}}cout<<f[m][v];return 0;
}

优化:

其中,我们发现其实f数组的第一维没有实际用处,因为i最后都会等于物品数。于是我们进行一个空间上的优化:把f换成一维数组f[j]表示背包容量为j的时候所能获得的最大价值。

状态转移方程:

f[j]=max(f[j],f[j-volume[i]]+value[i])

于是,我们得到以下代码:

#include<bits/stdc++.h>
using namespace std;
//w;重量  p;价值 
int f[200000],w[2000],p[2000],m,v;
int main(){cin>>v>>m;for(int i=1;i<=m;i++){cin>>w[i]>>p[i];}for(int i=1;i<=m;i++){for(int j=v;j>=w[i];j--){//从背包容积大小递减到当前物品体积f[j]=max(f[j],f[j-w[i]]+p[i]);}			}cout<<f[v];return 0;
}
//#include<bits/stdc++.h> // 引入标准库,包括输入输出、算法、容器等  
//using namespace std; // 使用标准命名空间,避免每次使用标准库时都需要加上std::前缀  
//  定义全局变量  w: 物品的重量数组  p: 物品的价值数组  m: 物品的数量  v: 背包的容量  
//int f[200000], w[2000], p[2000], m, v;  
//  
//int main() {  
//    // 输入背包的容量v和物品的数量m  
//    cin >> v >> m;  
//  
//    // 读取每个物品的重量和价值,并存储到对应的数组中  
//    for (int i = 1; i <= m; i++) {  
//        cin >> w[i] >> p[i];  
//    }  
//  
//    // 初始化动态规划数组f,这里虽然没有显式初始化,但C++的局部变量默认会初始化为0  
//    // f[j]表示当背包容量为j时,可以得到的最大价值  
//  
//    // 外层循环遍历每个物品  
//    for (int i = 1; i <= m; i++) {  
//        // 内层循环逆序遍历背包的容量,从最大容量v递减到当前物品的重量w[i]  
//        // 逆序遍历是为了保证在计算f[j]时,f[j-w[i]]是未考虑当前物品i时的状态  
//        for (int j = v; j >= w[i]; j--) {  
//            // 更新f[j]的值,考虑是否将当前物品i放入容量为j的背包中  
//            // 如果放入,则总价值为f[j-w[i]](未放入当前物品时的最大价值)加上p[i](当前物品的价值)  
//            // 如果不放入,则总价值仍为f[j]  
//            // 取两者中的较大值作为新的f[j]  
//            f[j] = max(f[j], f[j - w[i]] + p[i]);  
//        }  
//    }  
//  
//    // 输出当背包容量为v时的最大价值,即f[v]  
//    cout << f[v];  
//  
//    return 0; // 程序正常结束  
//}

值得注意的是,我们是对f数组进行“倒着遍历”的。因为01背包动态规划的基本原则就是现在循环枚举到的容量之前的必须是已经确定了的。如果我们正序遍历,f[j-w[i]]有可能是没有被状态转移过的值,进而使得答案错误。 

二、完全背包 

概述:

完全背包就是在01背包的基础上,引入了一个“物品数量”的概念,并且使得每个物品的数量为无限大。

实现: 

同样,我们定义一个二维数组,让dp[i][j]表示考虑第i个物品并且背包容量为j时的最大价值。

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int f[N][N],w[N],p[N],m,v;
int main(){cin>>v>>m;for(int i=1;i<=m;i++){cin>>w[i]>>p[i];}for(int i=1;i<=m;i++){for(int k=1;k<=v;k++)//循环物品的数量,因为背包的容积为v,我们最多就循环到v for(int j=1;j<=v;j++){if(w[i]*k<=j){f[i][j]=max(f[i][j],f[i-1][j-w[i]*k]+k*p[i]);}else{f[i][j]=max(f[i][j],f[i-1][j]);}			}}cout<<"max="<<f[m][v]<<endl;return 0;
}

但是请注意!!!!这并不是标准的完全背包的实现方式。大家也不要用这种方式,因为不稳定,它实际上是基于多重背包来实现的。下面,我们来看标准的方法:

#include<bits/stdc++.h>
using namespace std;
inline int read(){char w=getchar();int fl=1,sum=0;while(w>'9'||w<'0'){if(w=='-')fl=-1;w=getchar();}while(w<='9'&&w>='0'){sum=(sum<<1)+(sum<<3)+(w^48);w=getchar();}return fl*sum;
}
int t,m;
int ti[10010],va[10010],f[10010];
int main(){t=read();//背包容积 m=read();//物品数量 for(int i=1;i<=m;i++){ti[i]=read();va[i]=read();}for(int i=1;i<=m;i++){//循环每个物品 for(int j=1;j<=t;j++){//循环每个背包容量,注意是正序 if(j>=ti[i]){f[j]=max(f[j],f[j-ti[i]]+va[i]);//状态转移 }		}}cout<<"max="<<f[t]<<"\n";return 0;
}

同样值得注意的是,在使用一维数组解决完全背包问题时,我们对f数组进行正序遍历。为什么呢? 

在完全背包问题中,如果我们对f数组进行逆序遍历(就像01背包那样),我们可能会遇到一个问题:当我们尝试更新f[j]时,实际上我们可能已经使用了f[j]来更新f[j+w[i]]等更大的容量值。这会导致我们错误地多次计入同一个物品的价值,因为f[j]可能已经被更新为包含当前物品的状态。

然而,如果我们使用正序遍历,这个问题就不会发生。当我们遍历到f[j]时,我们还没有更新任何f[j'](其中j' > j),这意味着f[j-w[i]]仍然代表不包含当前物品时的最优解。因此,我们可以安全地将f[j-w[i]] + p[i](即不选当前物品的价值加上选择当前物品一个的价值)与f[j](即不选当前物品的价值)进行比较,以决定是否更新f[j]

三、多重背包 

概述:

和完全背包类似,只不过物品数量不是无限大,而是通过输入获取的

实现:

第一种请参考上述完全背包中的二维数组的实现方式,这里就不过多赘述了

例题:

让我们根据一道例题,来深刻领悟下完全背包。

洛谷传送门:P1776 宝物筛选 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

如果直接使用上述完全背包中二维数组的方式实现,会超时。那么我们来考虑一种优化的方式(也就是让我昏了一下午最后被神犇救活的东西)

<二进制优化>

 首先,我们先不管到底是怎么优化的。我们先来分析以下为什么按照原来那么写会超时

我们通过观察代码可以得出,

对于每个物品的数量,我们会从1开始对这个数量(设为s[i])进行累加,直到超过背包容量或者超过s[i]。

这个过程中,由于每次循环的步长为1,我们就一共会循环s[i]次。

然而对于同种物品,无论我们选择的是第一个,还是最后一个,本质上因为物品的体积和价值是一样的,那么我们可以认为这两种选择方案是等价的。

于是,我们可以通过减少循环次数(也就是不按照原来每一次加上1来遍历),而是通过对物品的数量、体积和价值进行预处理,将某个物品所有的组合方式用最基础的数量表示出来。而上述“最基础的”组合方式就是当前物品数量(s[i])转化为二进制数的一部分。这是因为二进制表示能够以最少的数字组合表示任何数量的物品。(任何数都可以由若干个2的幂次项组合而成,这些幂次项就构成了“最基础的”组合方式。)

代码实现(AC代码): 

#include<bits/stdc++.h>
using namespace std;
inline int read(){char w=getchar();int fl=1,sum=0;while(w>'9'||w<'0'){if(w=='-')fl=-1;w=getchar();}while(w<='9'&&w>='0'){sum=(sum<<1)+(sum<<3)+(w^48);w=getchar();}return fl*sum;
}
int n,m,v[10010],w1[10010],w[10010],s[20000],f[40010],cnt;
int v1[10010];
int main(){n=read();m=read();for(int i=1;i<=n;i++){v[i]=read();w[i]=read();s[i]=read();//num}for(int i=1;i<=n;i++){int nw=1,sum=1;while(sum<s[i]){w1[++cnt]=nw*w[i];v1[cnt]=nw*v[i];nw<<=1;sum+=nw;}sum>>=1;w1[++cnt]=(s[i]-sum)*w[i];//生成新物品的体积v1[cnt]=(s[i]-sum)*v[i];//生成新物品的价值}//01背包的问题for(int i=1;i<=cnt;i++){for(int j=m;j>=w1[i];j--){	f[j]=max(f[j],f[j-w1[i]]+v1[i]);}}cout<<f[m]<<"\n";return 0;
}

代码二进制优化部分解释(即第一个while):

 我们的想法是通过二进制编码的方式,对于每个物品数量s[i]进行一次编码(也就是分组),并且把每次分组的结果看成一个整体

即假如对于9个物品,我们将其分组,通过二进制分为1,2,4,2(实际上我们就是在将s[i]转化为二进制数的形式来进行分组,只不过在转化过程中,我们并不是直接得到一个单一的二进制数,而是将这个二进制数展开成若干个2的幂次项(即二进制中的每一位代表的数),以及一个可能的余数(如果s[i]不是2的幂次方的和的话)。到第四位的时候,9-1-2-3=2,很明显不符合2的三次方(即8),于是,我们单独把"余数"放进去),这样我们就得到了4个数,但是这四个数字通过加法运算可以得出1-9的任意一个数字。到这里,我们就完成了“分组”

最后,我们通过将每一组看作一个整体的方式,将新生成的物品(比如两个一组就把这两个看作一个新的物品,价值为2乘上原来物品的价值,体积为2乘上原来物品的体积)保存到新数组里面,然后通过对每一组物品 选与不选 的方式进行01背包的算法,得出最后的最优解。

其实二进制优化的核心就是通过对物品分组,达到将很多物品的多重背包问题转化为01背包问题的目的。

最后,再次感谢神犇的帮助(鞠躬)

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

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

相关文章

JNI编程二:JNI数据类型

目录 前言一、数据类型 jclass / jobject二、JNI常见的数据类型三、运用数据类型3.1 修改String类型的变量3.2 修改int类型的变量 前言 前面阐述了JNI的开发流程&#xff0c;接下来探究JNI中的数据类型。编码承接上文JNI编程一&#xff1a;JNI开发流程 一、数据类型 jclass /…

STM32——I2C和SPI波形分析

波形分析 I2C波形 //写命令 void OLED_WR_CMD(uint8_t cmd) { HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x00,I2C_MEMADD_SIZE_8BIT,&cmd,1,0x100); } //写数据 void OLED_WR_DATA(uint8_t data) { HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x40,I2C_MEMADD_SIZE_8BIT,&am…

第三届IEEE云计算、大数据应用与软件工程国际学术会议 (IEEE-CBASE 2024,10月11-13)

第三届IEEE云计算、大数据应用与软件工程国际学术会议 ( CBASE 2024 &#xff09;将于2024年10月11—13日在中国杭州举办。 该会议在连续两届成功举办的基础上&#xff0c;本届将由浙江水利水电学院、浙江省自动化学会、浙江省科协智能制造学会联合体主办&#xff0c;浙江水利水…

【轻松拿捏】设计模式六大基本原则(一)单一职责原则(SRP - Single Responsibility Principle)

&#x1f388;边走、边悟&#x1f388;迟早会好 一. 概述 单一职责原则&#xff08;SRP - Single Responsibility Principle&#xff09;是面向对象设计中的一个基本原则。它的核心思想是&#xff1a;一个类只应有一个引起它变化的原因&#xff0c;也就是说&#xff0c;一个类…

git clone报错unable to access

网页能够访问github.com和外网&#xff0c;git 也是安装了最新版&#xff0c;但是在使用 git clone xxx 时就报错&#xff1a; $ git clone https://github.com/XXX.git Cloning into XXX... fatal: unable to access https://github.com/XXXc.git/: OpenSSL SSL_connect: SSL…

C ++初阶:C++入门级知识点

目录 &#x1f31e;0.前言 &#x1f688;1.C输入输出 &#x1f688;2.缺省参数 &#x1f69d;2.1全缺省参数 &#x1f69d;2.2半缺省参数 &#x1f688;3.函数重载 &#x1f69d;3.1参数类型不同 &#x1f69d; 3.2参数个数不同 &#x1f69d;3.3参数类型顺序不同 ​…

相似度计算方法-编辑距离 (Edit Distance)

定义 编辑距离&#xff08;Edit Distance&#xff09;&#xff0c;也称为Levenshtein距离&#xff0c;是一种衡量两个字符串相似度的方法。它定义为从一个字符串转换为另一个字符串所需的最少单字符编辑操作次数&#xff0c;这些操作包括插入、删除或替换一个字符。 计算方法 …

Mysql(三)---增删查改(基础)

文章目录 前言1.补充1.修改表名1.2.修改列名1.3.修改列类型1.4.增加新列1.5.删除指定列 2.CRUD3.新增(Create)3.1.单行插入3.2.指定列插入3.3.多行插入 4.数据库的约束4.1.约束的分类4.2.NULL约束4.3.Unique约束4.4.Default 默认值约束4.5.PRIMARY KEY&#xff1a;主键约束4.6.…

谷哥剪映助手实操,批量自动化制作左右分屏视频

我给大家介绍如何用谷哥剪映助手&#xff0c;配合剪映批量制作左右分屏或上下分屏视频。 首先我们准备好剪映参考草稿&#xff0c;草稿里有主轨和复轨两条素材。一般情况下&#xff0c;副轨比主轨时长更长。剪映助手将根据主轨时长裁切副轨。 这里需要注意的是&#xff0c;在批…

十五年以来 — 战略性云平台服务的演进路径之全面呈现(含亚马逊、微软和谷歌)

Gartner每年都发布对全球IaaS平台进行评估的魔力象限报告。2023年底&#xff0c;Gartner将此项评估的名称改为“战略性云平台服务”&#xff08;Strategic cloud platform services&#xff09;&#xff0c;尽管其核心仍为IaaS&#xff0c;但是&#xff0c;毫无疑问&#xff0c…

90. UE5 RPG 实现技能的装配

在上一篇里&#xff0c;我们实现了在技能面板&#xff0c;点击技能能够显示出技能的相关描述以及下一级的技能的对应描述。 在这一篇里&#xff0c;我们实现一下技能的装配。 在之前&#xff0c;我们实现了点击按钮时&#xff0c;在技能面板控制器里存储了当前选中的技能的相关…

ZooKeeper工作原理

1. ZooKeeper工作原理 1.1 ZooKeeper角色 领导者&#xff08;Leader&#xff09;&#xff1a;在Zookeeper集群中&#xff0c;Leader是负责管理集群事务的节点。它负责处理所有的写请求&#xff0c;并将这些请求转化为事务&#xff0c;并提交事务日志。Leader节点还负责发起和决…

用C#写一个随机音乐播放器

form1中namespce里的代码如下 public partial class Form1 : Form {public Form1(){InitializeComponent();}private void button1_Click(object sender, EventArgs e){string folder textBox1.Text;string folderPath folder; // 指定音频文件所在的文件夹路径OpenRandomFi…

vue项目上线打包后出现的问题

1、出现空白页 1.1 打包路径&#xff1a; module.exports {publicPath:./, //修改为绝对路径 } 修改完打包路径后build可以展示页面 1.2 路由模式&#xff1a; 项目上线要求是history模式&#xff0c;需要后端做重定向 前端自测可以使用h…

JavaWeb笔记_FilterListener

一.过滤器 1.1 过滤器概述 过滤器主要用来拦截目标资源&#xff08;静态资源或动态资源&#xff09;的请求和响应 &#xff08;类似地铁的安检&#xff09; 我们访问动态或静态资源都要通过URL访问&#xff1a;http://localhost:8080/... 所以过滤器本质上拦截的是URL 1.2 过滤…

dps或者ppt文件判断是否加密

doc文件是否加密可以通过fib来判断&#xff0c;例如 同样的方法判断ppt也可以&#xff0c;但是在判断wps保存的dps文件时&#xff0c;提示没有加密&#xff0c;文件双击打开时又需要密码&#xff0c;查看ppt格式文档有下面发现 查看文件的二进制发现了加密标识 后面再研究doc x…

OpenCV图像滤波(4)构建图像金字塔函数buildPyramid()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在计算机视觉和图像处理中&#xff0c;构建图像金字塔&#xff08;Image Pyramid&#xff09;是一种常用的技术&#xff0c;它生成一系列分辨率逐…

CISAW信息安全保障人员认证是否值得学习?

CISAW信息安全保障人员认证的学习难度因人而异。 如果考生具备足够的学习能力以及丰富的信息安全工作经验&#xff0c;那么考试的难度可能会相对较低。 相反&#xff0c;如果考生缺少这些条件&#xff0c;学习难度可能会相对较高。 1. 从考试内容来看&#xff0c;CISAW以概念…

Neutralinojs教程项目实战初体验(踩坑指南),干翻 electron

Neutralinojs 项目实战初体验&#xff08;踩坑指南&#xff09;&#xff0c;干翻 electron Neutralinojs 官方文档 卧槽卧槽&#xff0c;&#xff01;这个年轻人居然用浏览器把电脑关机了_哔哩哔哩_bilibili正是在下 本教程搭建的是纯原生项目&#xff0c;没有和其它前端框架…

简单快捷!Yarn的安装与使用指南

Yarn 是由 Facebook (现 Meta) 开发的包管理工具。 今天&#xff0c;我将介绍如何使用 Yarn。 目录 Yarn 的官方网站 关于安装 版本确认 开始一个新项目&#xff08;创建 package.json 文件&#xff09; 安装软件包 升级包 运行脚本 执行包的命令 卸载包 总结 Yarn 的…