作者:指针不指南吗
专栏:算法篇或许会很慢,但是不可以停下来
acwing 背包问题——学习笔记
01背包、完全背包、多重背包、分组背包
Dp问题,先写出基本形式,然后优化,对代码进行等价变形
问题描述:
有 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
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
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;
}
问题描述:
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 v i v_i vi ,价值是 w i w_i wi 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
(1)问题分析
(2)代码实现
①基础版——二维/三重循环
问题分析部分已经讲的很清楚了
代码如下:
#include
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
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;
}
问题描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品 最多有 s i s_i si 件,每件体积是 v i v_i vi ,价值是 w i w_i wi 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
(1)问题分析
(2)代码实现
①基础版——二维/三重循环
上面问题分析讲的很清楚
代码如下:
#include
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
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[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;
}
题目描述:
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij ,价值是 w i j w_{ij} wij ,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
(2)代码实现
①基础版——二维
思路看上图
代码如下:
#include
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
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;
}