Acwing 背包问题

背包问题

首先,什么是背包问题?

    • 给定N个物品和一个容量为V的背包,每个物品有体积和价值两种属性,在一些限制条件下,将一些物品放入背包,使得在不超过背包体积的情况下,能够得到的最大价值。根据不同的限制条件,分为不同类型的背包问题。

1. 0-1背包问题

    • 给定N个物品和一个容量为V的背包,每个物品有两个属性,分别是它的体积v_i,和它的价值w_i,每件物品只能使用一次,问往背包里放入哪些物品,能够使得物品的总体积不超过背包的容量,且总价值最大。
      在这里插入图片描述

f(i,j)可以分为两个更小的集合,一种是不包含第i个物品,一种是包含第i个物品

  • 不包含第i个物品:就是从物品1-i中选择,但是不能包含第i个物品的最大价值,换句话就是从物品1-i-1中选择,总体积不超过j的最大价值,即f(i - 1, j)
  • 包含第i个物品:就是从物品1-i中选择,但是必须包含第i个物品的最大价值,那么可以认为最开始直接把i塞进背包,此时背包的容量变成了j - vi,价值变成了wi,由于第i个物品已经装进背包了,那么从1-i选就变成了从1-i-1选了,因此此时的最大价值就是f(i - 1, j - vi) + wi
    f(i, j)取两种情况的最大值,因此f(i, j)= max(f(i - 1, j), f(i - 1, j - vi) + wi)

Acwing 2.0-1背包问题
实现思路:求f(i,j),i从0开始枚举到n件物品,再用j从0开始枚举到最大体积m,由于包含i的集合可能不存在,因此先计算不包含i的集合,即f(i,j)=f(i-1,j),若当前的状态可以划分包含i的状态,即j>=v[i],那么就计算当前枚举的f(i,j)最终值,即max(f((i-1),j),f(i-1,j-v[i])+w[i])),当全部枚举结束后,计算的就是f[n][m],即前n个物品中总体积不超过m的最大价值。

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 1010;int n,m;
int v[N],w[N];
int f[N][N];//存状态int main(){cin >> n >> m;for(int i = 1 ; i <= n; i ++) cin >> v[i]  >> w[i] ;for(int i = 1; i <= n ; i++){//f[0][j]都默认为0for(int j = 0 ; j <= m; j ++){//f[i][0]都默认为0f[i][j] = f[i-1][j];//不包含物品i的情况if(j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);//包含物品i,直接先放进去}}cout << f[n][m] << endl;return 0;
}

优化:滚动数组优化为一维
将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。
为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的ij最优解,即放前i个物品在体积为j时的最大价值(很多个状态都可以得到),但题目只需要求得最终状态f[n][m](只要一个状态,不用求那么多状态),因此我们只需要一维的空间来更新状态

  • 状态f[j]定义:N件物品,背包容量j下的最优解(最大价值);
  • 注意枚举背包容量j必须从m开始,即逆序遍历处理不同体积。
    为什么一维情况下枚举背包容量需要逆序?
    在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
    那我们可以看看不倒序会怎样?
    在这里插入图片描述
    正序更新:

for (int j = v[i]; j <= m; j++) {f[j] = max(f[j], f[j - v[i]] + w[i]);
}

在这里插入图片描述

倒序更新的好处:为了防止物品被重复选择,我们使用倒序循环,从背包最大容量 m 逐渐递减到 v[i],这样在更新 f[j] 时,之前的 f[j - v[i]] 是上一轮未更新的旧值,这保证了每个物品只被使用一次。

for (int j = m; j >= v[i]; j--) {f[j] = max(f[j], f[j - v[i]] + w[i]);
}

在这里插入图片描述
具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 1010; 
int n, m; // n: 物品数量, m: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的重量, w[i]: 第i个物品的价值
int f[N]; // f[j]: 容量为j时的最大价值int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];// 动态规划过程for (int i = 1; i <= n; i++) {// 倒序循环,防止物品被重复选择,j逆序,到v[i]为止(小于vi就没意义,装不下vi)for (int j = m; j >= v[i]; j--) {f[j] = max(f[j], f[j - v[i]] + w[i]);}}cout << f[m] << endl;return 0;
}

2. 完全背包问题

相比0-1背包问题,完全背包问题的各个物品是无限个的,即放入背包的物品i可以不限数量(即可以重复使用i)
Acwing 3.完全背包问题
在这里插入图片描述
实现思路:和0-1背包问题的区别在状态计算中的集合划分,不是只有0和1,而是可以选k个
在这里插入图片描述
朴素做法:与0-1背包思路相同,只是在集合划分上有所区别,以f[i,j]为例,对其进行下一步划分,考虑以取ki物品划分集合,若k=0,则相当于f[i-1,j];若k不等于0,则采取01背包类似的办法,先确定取k个物品i,不影响最终选法的求解,即求f[i-1,j-k*v[i]],再加上k*w[i],即f[i-1,j-k*v[i]]+k*w[i],不难发现k=0情况可以与之合并,最终就是取从0枚举到k,最终状态转移方程为f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])的最大值,k的最大值可以通过j>=k*v[i]求解。有三重for循环,时间复杂度最差为O(n*m^2)

注意:这里max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]),而不是类似0-1背包那样取max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])。因为k=0时,即物品i不取的情况,完全背包方程就为max(f[i][j],f[i-1][j]),实质上就涵盖了f[i-1][j]的情况

具体实现代码:

#include <iostream>
#include <alforithm>
using namespace std;
const int N=1010;
int w[N],v[N],f[N][N];
int n,m;int main(){cin>>n>>m;for(int i=1;i<=n;i++) cin>>v[i]>>w[i];for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)for(int k=0;k*v[i]<=j;k++){f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);}cout<<f[n][m]<<endl;return 0;
}

但是现在数据加强了,会超时!那就进行优化!
二维优化版:改为二重循环,降低时间复杂度
像0-1背包那样考虑分成两种情况看待,

第一种情况:从i物品一个都不取开始;

第二种情况:从至少取一份i物品开始,即j-v

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
观察上面两式,括号中对应部分只相差一个w,可得出如下递推关系:
f[i][j]=max( f[i-1][j],f[i,j-v]+w )

所以可以去掉k,即去掉第三重循环

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];int main(){cin >> n >> m;for(int i = 1 ; i <= n ; i ++) cin >> v[i] >> w[i];for(int i = 1; i <= n ; i ++){for(int j = 0 ; j <= m ; j ++){f[i][j] = f[i-1][j];//不取物品iif(j >= v[i]) f[i][j] = max(f[i][j],f[i][j - v[i]] + w[i]);//至少取一份物品i}}cout << f[n][m] << endl;return 0;
}

滚动数组优化

观察可以发现可0-1背包的代码很像,所以可以像0-1背包那样用滚动数组优化。

区别在于:第二部分是i-1,还是i,即需要的值是上一轮的i-1还是本轮的i

f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包 需要i-1轮的值来更新

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题 需要i轮的值来更新

相比0-1背包,进行滚动优化区别j正序遍历处理了(而0-1背包的j是逆序)
具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 1010; 
int n, m;           // n表示物品数量,m表示背包的最大容量
int v[N], w[N];     // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值
int f[N];           // f[j]表示容量为j时的最大价值int main() {cin >> n >> m;for(int i = 1; i <= n; i++) {cin >> v[i] >> w[i];}// 动态规划求解完全背包问题for(int i = 1; i <= n; i++) { // 遍历每一个物品// 完全背包问题:每件物品可以被选择多次,因此体积从小到大遍历for(int j = v[i]; j <= m; j++) { // 对于容量为j的背包,判断是否选择第i个物品,取最大价值f[j] = max(f[j], f[j - v[i]] + w[i]); }}cout << f[m] << endl;return 0;
}

我们可以发现,完全背包问题和0-1背包问题就是差了一个体积的遍历顺序哦~(背包从小到大,0-1从大到小)

3.多重背包问题

每件物品的个数是不同的,比如,每件物品的个数是si个。

相比完全背包问题,只是每个物品的个数有了上限,不再是无限
Acwing 4.多重背包问题
在这里插入图片描述
在这里插入图片描述

朴素版本:和完全背包问题基本一样,只是k多了个上限限制,用数组s[]表示某个物品的上限。时间复杂度为O(NVS),数据量小的情况下可以AC。
具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 1010;
int v[N],w[N],s[N];
int f[N][N];
int n,m;int main(){cin >> n >> m;for(int i = 1 ;i <= n ; i ++) cin >> v[i] >> w[i] >> s[i];for(int i = 1 ;i <= n ; i ++){for(int j = 0 ; j <= m ; j ++){for(int k = 0; k <= s[i] && k * v[i] <= j ; k ++){//多一个上限的判断f[i][j] = max(f[i][j],f[i-1][j - k * v[i]] + k * w[i]);}}}cout << f[n][m] << endl;return 0;
}

利用二进制拆分将多重背包问题转换为多个01 背包问题,从而将原来的三重循环优化成两重循环。每个物品可以看作若干个不同的物品,这些物品的数量分别为 1, 2, 4, …, 直到总数不超过 s[i]。

Acwing 5.多重背包问题 II
在这里插入图片描述
优化步骤:

  • 将数量拆分为若干份:每种物品 i 有 s[i] 个,将其数量拆分为若干个 1、2、4、8… 的二进制份数,直到剩下的数量不足再处理。这种拆分方式保证了在所有的数量限制 s[i] 内完成计算,同时减少了重复的计算步骤。
  • 01背包的思想:每一份物品数量被视作一次01背包问题来处理,即将这份物品的体积和价值加入到状态转移中;
  • 状态转移
    • 设 f[j] 为背包容量为 j 时,能够获得的最大价值。
    • 对于物品 i 的第 k 份(二进制拆分后),其体积为 x * v[i],价值为 x * w[i],我们需要在背包容量允许的情况下更新状态,即 f[j] = max(f[j], f[j - x * v[i]] + x * w[i])。

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 2010; 
int v[N], w[N], s[N];  // v[] 存储物品的体积,w[] 存储物品的价值,s[] 存储物品的数量
int f[N];  // f[] 存储当前背包容量对应的最大价值
int n, m;int main() {cin >> n >> m;  for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];  // 使用二进制拆分对多重背包进行优化for (int i = 1; i <= n; i++) {int count = s[i]; // 当前物品 i 的数量限制for (int k = 1; count > 0; k = k * 2) {  // 通过二进制拆分处理数量int x = min(k, count);  // 每次拆分的数量为 1, 2, 4, 8,... 或不足的部分count -= x;  // 减去这次已经处理的物品数量// 类似于 01 背包问题的更新方式,倒序更新背包状态for (int j = m; j >= x * v[i]; j--) {f[j] = max(f[j], f[j - x * v[i]] + x * w[i]);}}}cout << f[m] << endl;return 0;
}

4.分组背包问题

有 N 组物品,每一组中有若干个物品每一组中至多选择一个

分组背包问题的思考方式和前面的类似。不同的地方仅仅在于状态转移。
Acwing 9.分组背包问题
在这里插入图片描述
分组背包问题的思考方式和前面的类似。不同的地方仅仅在于状态转移。

01背包的状态转移,是枚举第i个物品选或者不选;

完全背包和多重背包,是枚举第i个物品,选0,1,2,3,4,.... 个,无限个或有上限个
而分组背包,枚举的是第i个分组,选哪一个,或者一个都不选

在这里插入图片描述

  • 这里的体积数组v和价值数组w就要开成二维,表示某一组的某一个物品
  • 与01背包思路一致,集合划分为不包含i组,包含i组第1个物品,包含i组第2个物品,…包含i组第k个物品(k表示第i组的物品数量),…,包含第i组最后一个物品。因此若不包含第i组,则f(i,j)=f(i-1,j),若包含第i组第k个物品,则计算方法类似01背包(只是多了一重循环从i组里面选第k个物品),先除去第i组的第k个物品再进行计算的取法不变;
  • 分组背包的状态转移方程为:f[i][j]=max(f[i−1][j],f[i−1][j−v[i][k]]+w[i][k]), 1<k<s[i]。其中 v[i,k] 表示第 i 组中的第 k 个物品的体积,w [ i , k ] 同理。同样可以优化为一维:f[j]=max(f[j],f[j−v[i][k]]+w[i][k]),主要这里更新需要上一组的(和完全背包一样),j要逆序
    • j逆序枚举的最小值是1,不是0-1背包那样的v[i],因为i组里面的物品各自的体积都无法提前判断,不知道最小值

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int N = 110; int n, m; // n: 物品种类数, m: 背包容量
int v[N][N], w[N][N], s[N]; // v[i][j]: 第 i 种物品的第 j 个子物品的体积, w[i][j]: 
//第 i 种物品的第 j 个子物品的价值, s[i]: 第 i 种物品的子物品数量int f[N]; // 动态规划数组,f[j]: 容量为 j 的背包的最大价值int main() {cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> s[i]; for (int j = 0; j < s[i]; j++) { cin >> v[i][j] >> w[i][j]; }}// 动态规划更新过程for (int i = 1; i <= n; i++) { // 遍历每种物品for (int j = m; j >= 0; j--) { // 从背包容量 m 开始逆序遍历到 0for (int k = 0; k < s[i]; k++) { // 遍历当前物品组的每个子物品if (v[i][k] <= j) { // 判断当前子物品的体积是否可以放入背包// 更新最大价值:选择放入或不放入当前子物品f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);}}}}cout << f[m] << endl; return 0; 
}

以上就是各种背包问题,要学会分析状态表示和状态计算,推导出状态转移公式(这是关键)!

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

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

相关文章

【redis学习篇1】redis基本常用命令

目录 redis存储数据的模式 常用基本命令 一、set 二、keys pattern keys 字符串当中携带问号 keys 字符串当中携带*号 keys 【^字母】 keys * 三、exists 四、del 五、expire 5.1 ttl命令 5.2key删除策略 5.2.1惰性删除 5.2.2定期删除 六、type key的数据类型…

Windows安全加固详解

一、补丁管理 使用适当的命令或工具&#xff0c;检查系统中是否有未安装的更新补丁。 Systeminfo 尝试手动安装一个系统更新补丁。 • 下载适当的补丁文件。 • 打开命令提示符或PowerShell&#xff0c;并运行 wusa.exe <patch_file_name>.msu。 二、账号管…

Pikachu-Sql-Inject - 暴力破解

之前的破解&#xff0c;一般都需要 information_schema.schemata 、 information_schema.tables 、information_schema.columns 的权限&#xff0c;如果没有权限&#xff0c;就需要暴力破解&#xff1b; 如构造payload ,这个 abc 表就是我们要确定是否存在的表 vince and ex…

GPTQ vs AWQ vs GGUF(GGML) 速览和 GGUF 文件命名规范

简单介绍一下四者的区别。 参考链接&#xff1a;GPTQ - 2210.17323 | AWQ - 2306.00978 | GGML | GGUF - docs | What is GGUF and GGML? 文章目录 GPTQ vs AWQ vs GGUF&#xff08;GGML&#xff09; 速览GGUF 文件命名GGUF 文件结构文件名解析答案 附录GGUF 文件命名GGUF 文件…

Pandas基础学习

导入 导入pandas一般是这样导入的 import pandas as pdSeries 创建 s1 pd.Series([5, 17, 3, 26, 31])注意Series的第一个字母要大写&#xff0c;表明这其实是Series类的构建函数, 返回的是Series类的实例 获得元素或者索引 单独获得元素 s1.values单独获得索引值 s…

Flink 03 | 数据流基本操作

Flink数据流结构 DataStream 转换 通常我们需要分析的业务数据可能存在如下问题&#xff1a; 数据中包含一些我们不需要的数据 数据格式不方面分析 因此我们需要对原始数据流进行加工&#xff0c;比如过滤、转换等操作才可以进行数据分析。 “ Flink DataStream 转换主要作…

Kubernetes-环境篇-01-mac开发环境搭建

1、brew安装 参考知乎文章&#xff1a;https://zhuanlan.zhihu.com/p/111014448 苹果电脑 常规安装脚本&#xff08;推荐 完全体 几分钟安装完成&#xff09; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"苹果电脑 极…

【Android】中级控件

其他布局 相对布局RelativeLayout RelativeLayout下级视图的位置是相对位置&#xff0c;得有具体的参照物才能确定最终位置。如果不设定下级视图的参照物&#xff0c;那么下级视图默认显示在RelativeLayout内部的左上角。用于确定视图位置的参照物分两种&#xff0c;一种是与…

自动驾驶系列—全面解析自动驾驶线控制动技术:智能驾驶的关键执行器

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

JavaScript-上篇

JS 入门 JS概述 JavaScript&#xff08;简称JS&#xff09;是一种高层次、解释型的编程语言&#xff0c;最初由布兰登艾奇&#xff08;Brendan Eich&#xff09;于1995年创建&#xff0c;并首次出现在网景浏览器中。JS的设计初衷是为Web页面提供动态交互功能&#xff…

Leetcode - 140双周赛

目录 一&#xff0c;3300. 替换为数位和以后的最小元素 二&#xff0c;3301. 高度互不相同的最大塔高和 三&#xff0c;3302. 字典序最小的合法序列 四&#xff0c;3303. 第一个几乎相等子字符串的下标 一&#xff0c;3300. 替换为数位和以后的最小元素 本题直接暴力求解&a…

【hot100-java】【将有序数组转换为二叉搜索树】

二叉树篇 BST树 递归直接实现。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNo…

wpf实现新用户页面引导

第一步 第二部 部分代码: private void show(int xh, FrameworkElement fe, string con, Visibility vis Visibility.Visible) {Point point fe.TransformToAncestor(Window.GetWindow(fe)).Transform(new Point(0, 0));//获取控件坐标点RectangleGeometry rg new Rectangl…

Deathnote解题过程

主机扫描&#xff0c;发现192.168.1.194 arp-scan -l 端口扫描&#xff0c;发现80和22端口 nmap -sS 192.168.1.194 访问80端口发现自动跳转到http://deathnote.vuln/wordpress添加绑定地址就可以访问了 vim /etc/hosts 192.168.1.194 deathnote.vuln 访问发现并没有什么东西…

无人值守安装文件

一般配合BypassUAC 原理 一些 windows 无人值守安装文件中含有用户的明文或 base64 编码后的密文。 无人值守安装文件中的密码不一定是管理员密码&#xff0c;它也可能是普通用户的密码或者服务账户的密码。 不过&#xff0c;在无人值守文件&#xff08;如 sysprep.xml 或 …

11.数据结构与算法-线性表的案例分析与实现

案例2.1-一元多项式的运算 案例2.2-稀疏多项式的计算 图书信息管理系统

2-112基于matlab的协同干扰功率分配模型

基于matlab的协同干扰功率分配模型&#xff0c;带操作界面的功率分配GUI&#xff0c;可以实现对已有功率的分配优化&#xff0c;可以手动输入参数值。4个干扰山区分二批总干扰功率&#xff0c;每个扇区包括威胁总系数、综合压制概率、目标函数增量等。程序已调通&#xff0c;可…

NVIDIA NVLink-C2C

NVIDIA NVLink-C2C 文章目录 前言一、介绍1. 用于定制芯片集成的超快芯片互连技术2. 构建半定制芯片设计3. 使用 NVLink-C2C 技术的产品 二、NVLink-C2C 技术优势1. 高带宽2. 低延迟3. 低功率和高密度4. 行业标准协议 前言 将 NVLink 扩展至芯片级集成 一、介绍 1. 用于定制芯…

【Java的SPI机制】Java SPI机制:实现灵活的服务扩展

在Java开发中&#xff0c;SPI&#xff08;Service Provider Interface&#xff0c;服务提供者接口&#xff09;机制是一种重要的设计模式&#xff0c;它允许在运行时动态地插入或更换组件实现&#xff0c;从而实现框架或库的扩展点。本文将深入浅出地介绍Java SPI机制&#xff…

Linux驱动开发(速记版)--设备模型

第八十章 设备模型基本框架-kobject 和 kset 80.1 什么是设备模型 设备模型使Linux内核处理复杂设备更高效。 字符设备驱动适用于简单设备&#xff0c;但对于电源管理和热插拔&#xff0c;不够灵活。 设备模型允许开发人员以高级方式描述硬件及关系&#xff0c;提供API处理设备…