参考链接:
背包九讲经典博客:dd大牛的《背包九讲》
参考视频(B站大神敲代码):背包九讲专题
三种 背包问题:
1.01背包问题(每种物品只能选一次)
2.完全背包问题(每种物品都可以选无限次)
3.多重背包问题(每种物品具有不同的选择上限)
(参考链接:https://www.acwing.com/problem/content/2/)
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
输入样例 4 5
1 2
2 4
3 4
4 5输出样例:
8
01背包问题是所有背包问题的基础,之后的问题都可以在此基础之上变化,所以一定要理解清楚。尤其是对待不同问题,找出状态转移矩阵是解题的关键。
解法归纳:
文字描述:
1.如果装不下当前物品,那么前i个物品的组合和前i-1个物品的最佳组合是一样的
2.如果装得下当前物品
假设一:装当前物品:在给当前物品预留了相应空间的情况下,前i-1个物品预留了空间的的最佳组合加上当前物品的价值就是总价值
假设二:不装当前物品,那么前i个物品的组合和前i-1个物品的最佳组合是一样的 取假设一和假设二中较大的价值,就是当前最佳价值的组合
符号表示:
f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值
状态转移矩阵为:f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v-c[ i ] ]+w[ i ] }
直接用公式表示可能会有些抽象,接下来用题目中的具体事例来演示过程
用一个二维数组来记录过程,其中
列的范围为1~5,表示背包的容量
行的范围为1~4,每行代表一个物品
按照从左到右,从上到下的顺序,记录得到的表格如下:
背包容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
1 | 0 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 2 | 4 | 6 | 6 | 6 |
3 | 0 | 2 | 4 | 6 | 6 | 8 |
4 | 0 | 2 | 4 | 6 | 6 | 8 |
暴力解法,二维数组
时间复杂度:O(NV)
空间复杂度:O(NV)
#include
#include
using namespace std;
//注意此处的N至少要比最大范围多1
const int N=1001;
int f[N][N];
int V[N];
int W[N];
int main()
{
int n,v;
cin>>n>>v;
//存储输入
for(int i=1;i<=n;i++)
cin>>V[i]>>W[i];
for(int i=1;i<=n;i++)
{
//背包容量需从0,...,v
for(int j=0;j<=v;j++)
{
//装不下当前物品
if(j<V[i])
f[i][j]=f[i-1][j];
//假设一与假设二中取最大值
else
f[i][j]=max(f[i-1][j],f[i-1][j-V[i]]+W[i]);
}
}
//最终的答案并不一定是f[n][v],而是f[n][0...v]的最大值
int res=0;
for(int j=0;j<=v;j++)
res=max(res,f[n][j]);
cout<<res<<endl;
return 0;
}
优化空间复杂度,一维数组
时间复杂度:O(NV)
空间复杂度:O(V)
根据状态转移矩阵f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v-c[ i ] ]+w[ i ] } 可知:
计算 f[ i ][ 0,…,V ] 时,只需访问 f[ i-1 ][ 0,…,V ],即二维矩阵的第 i 行计算只依赖于第 i-1 行的数据,无需存储整个二维数组,用一维数组不断迭代即可实现,类似于斐波那契数列的优化解法。
新的转态转移矩阵:f[ v ] = max( f[ v ] , f[ v-c[ i ] ] + w[ i ] )
在代码的实现中采用倒序遍历可以实现此过程,由于更新f[ v ] 使用的是更小的f[v-…],所以
等式左边的f[ v ] 即为f[ i ][ v ](最新更新结果)
等式右边的f[ v ] 即为f[ i-1 ][ v ](上一次更新结果)
#include
#include
using namespace std;
const int N=1001;
int f[N];
int V[N];
int W[N];
int main()
{
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++)
cin>>V[i]>>W[i];
//简化版一维数组
for(int i=1;i<=n;i++)
{
//采用倒序遍历 0,...,v
for(int j=v;j>=V[i];j--)
f[j]=max(f[j],f[j-V[i]]+W[i]);
}
cout<<f[v]<<endl;
return 0;
}
(参考链接:https://www.acwing.com/problem/content/3/)
有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 输入样例 4 5
1 2
2 4
3 4
4 5
输出样例:10
完全背包问题与01背包问题的关系非常巧妙,
条件上只是每个物品可以选一次和无数次的区别
在代码实现时,只需将倒序遍历改变为顺序遍历即可
01背包问题中要按照v=V…0的逆序来循环。
这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。
而完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0…V的顺序循环。这就是这个简单的程序为何成立的道理
此时用二维数组表示即为,
(可以从下方表格看出,最优解是选了5次第一个物品)
背包容量 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
1 | 0 | 2 | 4 | 6 | 8 | 10 |
2 | 0 | 2 | 4 | 6 | 8 | 10 |
3 | 0 | 2 | 4 | 6 | 8 | 10 |
4 | 0 | 2 | 4 | 6 | 8 | 10 |
#include
#include
using namespace std;
int f[1001];
int main()
{
int N,V;
cin>>N>>V;
for(int i=0;i<N;i++)
{
int v,w;
cin>>v>>w;
//顺序遍历,0,...,V
for(int j=v;j<=V;j++)
f[j]=max(f[j],f[j-v]+w);
}
cout<<f[V]<<endl;
return 0;
}
(参考链接:https://www.acwing.com/problem/content/4/)
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 输入样例 4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:10
最基本的思路为,最优解中可以包含 0,1,2,…,s 第 i 个物品
状态转移矩阵为:f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v- k * c[ i ] ]+k*w[ i ] }
(k=0,1,2,…,s)
空间复杂度为:O(V*∑s[i])
上述思路的方法是将第 i 个物品拆分为1,2, …,s,对应的质量与价值也乘以相应的倍数,每个不同倍数的物品 i 就是一个全新的物品,然后就转换为了01背包问题。继续按照该思路,是否有更加高效的拆分方法?
二进制思想:
参考二进制数的表示方法,将每件物品的数量都可以用1,2,4,8,… 的2^k组合来表示。
例如13,则0~13范围内的所有数都可以用1,2,4,6,四个数来表示,其中6=13-(1+2+4)。这样13就由之前拆分为13个物品,简化为拆分4个物品。
空间复杂度:O(V*∑log n[i])
暴力解法
#include
#include
using namespace std;
int f[101];
int main()
{
int N,V;
cin>>N>>V;
for(int i=0;i<N;i++)
{
int v,w,s;
cin>>v>>w>>s;
for(int j=V;j>=v;j--)
{
//添加物品数量遍历层
for(int k=1;k*v<=j && k<=s;k++)
f[j]=max(f[j],f[j-k*v]+w*k);
}
}
cout<<f[V]<<endl;
return 0;
}
二进制解法
#include
#include
#include
using namespace std;
int f[2001];
struct use
{
int v;
int w;
};
int main()
{
int N,V;
cin>>N>>V;
vector<use> goods;
for(int i=0;i<N;i++)
{
int v,w,s;
cin>>v>>w>>s;
//二进制法对商品数量拆分
for(int k=1;k<=s;k*=2)
{
s=s-k;
goods.push_back({
k*v,k*w});
}
//s-∑2^k
goods.push_back({
s*v,s*w});
}
//转换为01背包问题
for(auto good:goods)
{
for(int j=V;j>=good.v;j--)
f[j]=max(f[j],f[j-good.v]+good.w);
}
cout<<f[V]<<endl;
return 0;
}