作者:指针不指南吗
专栏:算法篇🐾或许会很慢,但是不可以停下来🐾
文章目录
- 引入Dp
- 1.01背包
- 2.完全背包
- 3.多重背包
- 4.分组背包
acwing 背包问题——学习笔记
01背包、完全背包、多重背包、分组背包
引入Dp
Dp问题,先写出基本形式,然后优化,对代码进行等价变形
- 下面是Dp问题的分析基本流程
1.01背包
问题描述:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
(1)问题分析
「0-1 背包」即是不断对第 i 个物品的做出决策,「0-1」正好代表不选与选两种决定。
(2)代码实现
①基础版——二维
-
f[i][j]
表示前 i 个物品,背包容量 j 下的最优解(最大价值) -
当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:
f[i][j]=f[i-1][j]
-
当前背包容量够,两种选择:第 i 个物品,放或者不放,取最大值:
max(f[i - 1][j], f[i - 1][j - v[i]] + w[i])
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=1010; int n,m; //n表示物品个数,m表示最大容量 int v[N],w[N]; //v表示体积,w表示价值 int f[N][N]; //f[i][j]表示前i个物体,前j个容量的最大价值 int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; //f[0][0~m] 表示0个物体,容量都是0,因为是全局变量,省略 for(int i=1;i<=n;i++) for(int j=0;j<=m;j++) { f[i][j]=f[i-1][j]; if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]); } cout<<f[n][m]<<endl; return 0; }
②优化版——一维
-
整个转移方程中对于 i 这一维,只用到了i -1, 所以不需要记录所有的f[i],只需要用单个变量记录即可(滚动数组),去除 f 数组的 i 这一维
-
j 使用逆序枚举。如果我们按照正序枚举背包容量 j,即从小到大枚举,那么在更新
f[i][j]
时,可能会使用到f[i][j-w[i]]
这个状态,其中w[i]
表示第 i 个物品的重量。这相当于在容量为j-w[i]
的情况下再次放入物品 i,与题目要求的 0/1 背包问题不符。 -
因此,为了避免每个物品多次被放入背包的情况,我们采用逆序枚举背包容量 j 的方式更新 DP 状态。在逆序枚举的过程中,我们保证
f[i][j]
在f[i][j-w[i]]
之前被更新,从而确保每个物品只会被放入背包一次。代码如下:
#include<bits/stdc++.h> using namespace std; const int N=1010; int n,m; //n表示物品个数,m表示最大容量 int v[N],w[N]; //v表示体积,w表示价值 int f[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=m;j>=v[i];j--) { f[j]=max(f[j],f[j-v[i]]+w[i]); } cout<<f[m]<<endl; return 0; }
2.完全背包
问题描述:
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 v i v_i vi ,价值是 w i w_i wi 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
(1)问题分析
(2)代码实现
①基础版——二维/三重循环
-
问题分析部分已经讲的很清楚了
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=1010; int n,m; //n表示物品个数,m表示最大容量 int v[N],w[N]; //v表示体积,w表示价值 int f[N][N]; //f[i][j]表示前i个物体,前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=0;j<=m;j++) for(int k=0;k*v[i]<=j;k++) //k表示第i个物品数量 f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); cout<<f[n][m]<<endl; return 0; }
②优化版——一维/两重循环
-
发现规律
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,...) 由上两式,可得出如下递推关系: f[i][j]=max(f[i,j-v]+w,f[i-1][j])
进行优化
for(int i = 1 ; i <=n ;i++) for(int j = 0 ; j <=m ;j++) { f[i][j] = f[i-1][j]; if(j-v[i]>=0) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); }
-
与01背包进行对比
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i])
01背包f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i])
完全背包问题并进一步优化
for(int i = 1 ; i<=n ;i++) for(int j = v[i] ; j<=m ;j++)//这里的j是从小到大枚举,和01背包不一样 { f[j] = max(f[j],f[j-v[i]]+w[i]); }
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=1010; int n,m; //n表示物品个数,m表示最大容量 int v[N],w[N]; //v表示体积,w表示价值 int f[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=v[i];j<=m;j++) f[j]=max(f[j],f[j-v[i]]+w[i]); cout<<f[m]<<endl; return 0; }
3.多重背包
问题描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品 最多有 s i s_i si 件,每件体积是 v i v_i vi ,价值是 w i w_i wi 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
(1)问题分析
(2)代码实现
①基础版——二维/三重循环
-
上面问题分析讲的很清楚
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=110; int n,m; int v[N],w[N],s[N]; int f[N][N]; 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; }
②优化版—一维 /二进制
-
当数据很大时, O ( n 3 ) O(n^3) O(n3) 会超时,进行二进制优化
题解链接: AcWing 5. 二进制优化,它为什么正确,为什么合理,凭什么可以这样分?? - AcWing
讲一下为什么二进制优化可以哈。
题目的意思是某物品最多有s件,我们需要从所有的物品中选择若干件,使这个背包的价值最大。题目并没有说某物品一定需要选多少件出来,也没有说一共要选多少件出来。只是选择若干件,至于选几件,无所谓,但要保证价值最大。
按照优化的策略某物品有s件,我们给其打包分成了好几个大的物品。
第一个大物品是包含原来该物品的1件,第二个大物品是包含原来该物品的2件,第三个大物品是包含原来该物品的4件,第四个大物品是包含原来该物品的8件,…依次类推。此时我们就把所有的物品都重新进行了一个分类。
原先每个物品最多s件,我们就把这个件数条件给消去了。取而代之的是,按照一定原先件数组合出来的新若干大物品。
我们又已知按照我们划分成大物品进行搭配组合,一定能转化为原先的若干件小物品出来。
并且选择某物品的最多件数,是不会超过原先该物品的s件。所以就转化为从下面这些若干件大物品中,选择能使背包容积最大大的情况下,价值最高。这个就是一个01问题。
转自知名网友评论
代码如下:
#include<iostream> using namespace std; const int N = 12010, M = 2010; int n, m; int v[N], w[N]; //逐一枚举最大是N*logS int f[M]; // 体积<M int main() { cin >> n >> m; int cnt = 0; //分组的组别 for(int i = 1;i <= n;i ++) { int a,b,s; cin >> a >> b >> s; int k = 1; // 组别里面的个数 while(k<=s) { cnt ++ ; //组别先增加 v[cnt] = a * k ; //整体体积 w[cnt] = b * k; // 整体价值 s -= k; // s要减小 k *= 2; // 组别里的个数增加 } //剩余的一组 if(s>0) { cnt ++ ; v[cnt] = a*s; w[cnt] = b*s; } } n = cnt ; //枚举次数正式由个数变成组别数 //01背包一维优化 for(int i = 1;i <= n ;i ++) 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; }
4.分组背包
题目描述:
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij ,价值是 w i j w_{ij} wij ,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
(1)问题分析
(2)代码实现
①基础版——二维
-
思路看上图
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=110; int f[N][N]; int v[N][N],w[N][N],s[N]; int n,m,k; 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=0;j<=m;j++) { f[i][j]=f[i-1][j]; //不选 for(int k=0;k<s[i];k++) { if(j>=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]); //01背包 } } } cout<<f[n][m]<<endl; }
②优化版—一维
-
含有01背包,进行一维优化
代码如下:
#include<bits/stdc++.h> using namespace std; const int N=110; //使用的上一层的f,从大到小枚举体积,使用本层,从小到大枚举体积 //使用上一层,保证我们算这个所用到的体积,还没有被计算过,所以是存的上一层的状态 int n,m; int v[N][N],w[N][N],s[N]; int f[N]; 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--) //这里,使用的上一层,从大到小枚举 for(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; }