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

news2024/9/22 15:52:13

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

一、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.coloradmin.cn/o/2039087.html

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

相关文章

硬件开发流程

1.看原理图找引脚 --开发板上找到LED&#xff0c;查看丝印 --在原理图中根据丝印找到对应的器件 --找到对应的引脚 2.配置引脚功能(对应硬件的工作原理) 3.控制对应的引脚(或控制器) volatile: 易失性修饰符

qtpdfium 多平台编译

源码下载地址&#xff1a;https://codeload.github.com/kkzi/qpdf/zip/2681018e300738d6da9a9f89f06c93fc3ef17831 参考&#xff1a;https://blog.51cto.com/u_2194662/5256871 开发环境&#xff1a;QT 5.15.2 1. windows下编译&#xff1a; 编译环境&#xff1a;vs2019qt5.15…

【Python】线性规划模型(笔记)

线性规划的作用 求一个线性目标函数在线性可行域内的最值问题 线性规划的典型应用 配送运输问题&#xff1a;选大车还是小车生产规划问题&#xff1a;每种原料各买多少几何切割问题&#xff1a;切割长宽各多少买卖利润问题&#xff1a;最多能挣多少钱… 线性规划的本质 问…

6 二进制、十进制、八进制和十六进制的概念与进制转换,原码、反码、补码的概念与计算

目录 1 进制 1.1 常见的进制 1.2 不同进制的整数在 C 语言中的写法 1.3 printf 以不同进制形式输出整数 1.4 案例&#xff1a;使用不同的进制形式输出同一整数 2 进制的转换 2.1 二进制与十进制转换 2.1.1 二进制转换成十进制 2.1.2 十进制转换成二进制 2.2 八进制和…

pygame 赛亚人打怪

笔者最近学了一点pygame&#xff0c;献丑了。 代码附上&#xff0c;大佬勿喷&#xff01; import random import timeimport pygameWIDTH 410 HEIGHT 750 ENEMY_BORN pygame.USEREVENT 1 BULLET_BORN pygame.USEREVENT 10 ENEMY_BULLET_BORN pygame.USEREVENT 100 REW…

2024年最新版小程序云开发数据模型的开通步骤,支持可视化数据库管理,支持Mysql和NoSql数据库,可以在vue3前端web里调用操作

小程序官方又改版了&#xff0c;搞得石头哥不得不紧急的再新出一版&#xff0c;教大家开通最新版的数据模型。官方既然主推数据模型&#xff0c;那我们就先看看看新版的数据模型到底是什么。 一&#xff0c;什么是数据模型 数据模型是什么 数据模型是一个用于组织和管理数据的…

Linux信号控制进程种类、内存查看和NICE优先级

1.信号种类 1&#xff09;SIGHUP 重新加载配置 2&#xff09;SIGINT 键盘中断CtrlC 3&#xff09;SINGQUIT 键盘退出Ctrl\ 9&#xff09;SIGKILL 强制终止 15&#xff09;SIGTERM 正常结束终止 18&#xff09;SIGCONT 继续 19&#xff09;SIGSTOP…

使用克隆软件克隆Windows 10 硬盘

为什么 Windows 用户要克隆硬盘驱动器 您打算将旧硬盘升级为新硬盘吗&#xff1f;是否可以将操作系统迁移到 SSD&#xff1f;如何制作硬盘的相同副本&#xff1f;如何安全地升级操作系统而不会出现可启动故障问题&#xff1f;是否有任何安全便捷的方法可用于传输数据并升级到更…

Linux 中断机制(二)之中断子系统框架

目录 一、概述二、中断控制器 GIC1、Distributor 接口2、Redistributor 接口3、CPU 接口 三、中断类型四、中断号五、中断申请函数1、request_irq 函数2、request_threaded_irq() 函数3、gpio_to_irq 函数4、free_irq 函数 六、中断服务函数 一、概述 一个完整的中断子系统框架…

zdppy+vue3+onllyoffice开发文档管理系统实战 20240814上课笔记 验证码功能的进一步优化

遗留问题 1、实现验证码的功能2、要记录登录的Token和用户名&#xff0c;跳转到首页3、注册功能4、用户管理5、角色管理6、权限管理7、分享功能 当前进度 目前我们已经封装了zdppy_captcha这个框架&#xff0c;这个框架是专门用来生成验证码的。 缺少一些功能&#xff1a; …

024集——批量复制文字样式、改变文本文字样式——vba代码实现

CAD vba 不可直接修改文字样式的名称&#xff0c;可复制文字样式&#xff0c;文字样式名称前加特定前缀 要为对象改变文字样式&#xff0c;可使用 StyleName 属性。 If ent.ObjectName "AcDbText" Then ent.StyleName "新的" Set sel creatsel("…

【C#】读取与写入txt文件内容

在 C# 中读取和写入文本文件内容是一个常见的任务。以下是使用几种不同方法读取和写入文本文件的示例。 一、读取txt文件内容 1.1 使用 StreamReader using System; using System.IO;class Program {static void Main(){string filePath "C:\path\to\your\file.txt&qu…

【Unity打包Android】Gradle报错,Deprecated Gradle features were used in this build ···

Unity 2020.3.41f1c1 打包Android 加入Google Admob广告SDK后&#xff0c;打包Android失败&#xff0c;具体报错如下&#xff1a; 报错1&#xff1a; Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details> Configure project :l…

Hutool找出对象修改前后的字段变化

首先Maven引入Hutool依赖&#xff1a; <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.21</version> <!-- 请根据实际最新版本调整 --></dependency> 测试&#xff1a; 1…

OpenAI API error: “Unrecognized request argument supplied“

题意&#xff1a;OpenAI API 错误&#xff1a;‘提供了无法识别的请求参数’ 问题背景&#xff1a; Im receiving an error when calling the OpenAI API. Its not recognizing file argument, which I submitted to the API. 我在调用 OpenAI API 时遇到错误。API 不识别我提…

python语言day6 os random datetime .ini文件

os&#xff1a; 获取运行当前py文件的绝对路径&#xff1a; abspath中添加路径&#xff0c;会直接和绝对路径拼接。 folder_path os.path.abspath("") print(folder_path) 路径拼接&#xff1a; mac系统路径&#xff1a;file/TranslucentTB win系统路径&#xff1a;…

日撸Java三百行(day23:使用具有通用性的队列)

目录 前言 一、基础知识准备 1.Object类 2.Integer类 2.1包装类 2.2装箱和拆箱 2.3Integer类的常见方法 二、代码实现 1.队列创建及初始化 2.方法创建 3.数据测试 4.完整的程序代码 总结 前言 在昨天&#xff0c;我们使用了两个队列来辅助完成二叉树的“压缩顺序…

Android gradle 构建

Understanding Tasks - Gradle task kapt 是 Kotlin 语言的注解处理器&#xff0c;它是 Android Studio 中用于处理 Kotlin 注解的工具。它通过在编译期间生成代码来增强 Kotlin 代码的功能。需要 Kotlin 编译器来解析和处理注解&#xff1b;使用 APT 来生成代码&#xff0c…

【通天星主动安全监控云平台信息泄露漏洞】

目录 一、漏洞简介 二、资产测绘 三、poc利用 四、脚本批量验证 一、漏洞简介 “通天星主动安全监控云平台”是一个基于云计算技术的安全监控平台&#xff0c;通常用于保障网络安全、工业控制系统安全或物联网设备的安全。该信息泄露漏洞位于接口&#xff1a;/808gps/Stand…