目录
一、问题分析
1.01背包
01背包问题模型:给定N个物品,其中第i个物品的体积为Vi,价值为Wi。有一容积为M的背包,要求选择一些物品放入背包,使得物品总体积不超过M的前提下,物品的价值总和最大。
分析:依据其N个物品选择放入背包,对该问题进行分解,分成n类子问题,其阶段划分为当前第i个背包。其状态表示的集合为F[i,j],表示从前i个物品中选出了总体积为j的物品放入背包,物品的价值和,状态表示的属性为max。状态的转移方式由选或不选第i个物品组成。状态转移方程为F[i,j]=max(F[i-1,j],F[i-1,j-Vi]+Wi if(j>=Vi) ),0<=j<=M。初始化F[0,0]=0,所求为F[N][M]。
分析如图1所示。
![](https://i-blog.csdnimg.cn/blog_migrate/3d4dcea7768a101622404d9925680e2f.png)
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j]; ///不选第i个物品
///选择第i个物品
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
优化:通过分析状态转移方程发现,当前状态只和i-1行的状态有关,则其转移方程可化简为F[j]=max(F[j],F[j-Vi]+Wi),j>=Vi。其中j由M枚举到Vi。因为更新方式为倒序,阶段更新由后面开始。则当前F[j]实际存储的是F[i-1][j],F[j-Vi]存储的是F[i-1][j-Vi],则F[j]的转移方程等价于二维的F[i,j]转移方程。初始化F[0]=0,所求为F[M]。
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]);
}
}
2.完全背包
完全背包问题:给定N种物品,其中第i个物品的体积为Vi,价值为Wi,并且有无数个。有一容积为M的背包,要求选择若干个物品放入背包,使得物品总体积不超过M的前提下,物品的价值总和最大。
分析:其阶段划分为当前选取的物品i,状态表示集合为F[i,j],表示从前i个物品中选出若干物品其总体积为j放入背包的价值总和,状态表示属性为max。阶段间的状态转移方式为,选多少个第i个物品。状态转移方程为F[i,j]=max(F[i-1,j],F[i-1,j-Vi]+Wi,F[i-1,j-2*Vi]+2*Wi,...,F[i-1,j-k*Vi]+k*Wi),0<=j<=M。初始化为F[0,0]=0,所求为F[N][M]。
分析如图2所示。
![](https://i-blog.csdnimg.cn/blog_migrate/f119305ef01311ec02ff2eba6dc80861.png)
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j]; ///不选该物品
for(int k=1;k*v[i]<=j;k++) ///选择k个物品
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
优化:通过观察发现,F[i,j-Vi]=max(F[i-1,j-Vi],F[i-1,j-2*Vi]+Wi,F[i-1,j-3*Vi]+2*Wi,...,F[i-1,j-k*Vi]+(k-1)*Wi)加上Wi,等价于F[i,j-Vi]=max(F[i-1,j-Vi]+Wi,F[i-1,j-2*Vi]+2*Wi,...,F[i-1,j-k*Vi]+k*Wi),则F[i,j]=max(F[i-1,j],F[i,j-Vi]+Wi)。依据01背包所分析的,F[i,j]可以去掉i,则有F[j]=max(F[j],F[j-Vi]+Wi),Vi<=j<=M。因为F[j-Vi]实际为F[i,j-Vi],是从i阶段转移过来的,故j的更新要先将前面的i-1阶段的更新为i阶段的,故遍历顺序要从Vi到M。
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]);
}
}
3.多重背包
多重背包问题:给定N个物品,其中第i种物品的体积为Vi,价值为Wi,并且有Ci个。有一个容积为M的背包,要求选择若干物品放入背包,使得物品总体积不超过M的前提下,物品的价值总和最大。
分析:
方法1,直接拆分法。将每种物品内的Ci个物品都当成是独立的一个物品,然后对这些独立的物品当成共有个物品的01背包问题。
for(int i=1;i<=n;i++){
for(int k=1;k<=c[i];k++){ ///第i种物品内的Ci个物品
for(int j=m;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
}
方法2,二进制拆分法。每种物品内的Ci个物品有<=Ci,其中R为2的整次幂相加剩下来的数即R=Ci-(
)。因为0~
可以由
选择几个数相加得到。而0~
等价于0~Ci,可以由
选择几个数相加得到。故可以将每种物品内Ci个物品,分成这p+2个物品,组合而成。
vector<Good> goods;///goods({int,int})
while(n--){
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
///二进制拆分物品
for(int k=1;k<=s;k<<=1){ ///k为2的整次幂
s-=k; ///当前物品个数减去2的整次幂
goods.push_back({k*v,k*w});
}
if(s>0) goods.push_back({s*v,s*w}); ///将剩余物品加入goods中
}
for(auto x:goods){ ///将每个物品循环
for(int j=m;j>=x.v;j--){ ///尝试选择当前物品x
f[j]=max(f[j],f[j-x.v]+x.w);
}
}
4.分组背包
分组背包问题:给定N组物品,其中第i组有Ci个物品。第i组的第j个物品的体积为Vij,价值为Wij。有一容积为M的背包,要求选择如干个物品放入背包,使得每组至多选择一个物品并且物品总体积不超过M的前提下,物品的价值总和最大。
分析:将物品组数作为阶段划分,其状态表示集合为F[j],表示选择总体积为j的物品放入背包,物品的价值和,状态表示属性为max。对于每一组内的数个物品,做出的状态转移方式为不选物品或选择一个物品。故状态转移方程为F[j]=max(F[j],F[j-Vik]+Wik),1<=k<=Ci。其中因为阶段转移是由i-1转移到i,故j的遍历顺序为倒序,由M>=j>=0。
分析如图3所示。
![](https://i-blog.csdnimg.cn/blog_migrate/6c57f83052bba62675d5027908a1dc18.png)
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){ ///因为每个背包只能选择一个物品,故要放在外层
for(int k=1;k<=c[i];k++){
if(j>=v[i][k]){
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
}
二、例题实战
1.01背包
a.题目描述
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
b.代码实现
```
///二维
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],f[N][N];
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&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];///不选物品i
///选择物品i
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
///输出从前n个物品里面选,物品总体积为m的最大值
cout <<f[n][m] <<endl;
return 0;
}
```
```
///一维
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],f[N];
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&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]);
}
}
///输出物品总体积为m的最大值
cout <<f[m] <<endl;
return 0;
}
```
2.完全背包
a.题目描述
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 ii 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
b.代码实现
```
///二维
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],f[N][N];
int n,m;
int main(){
cin >>n >>m;
for(int i=1;i<=n;i++) scanf("%d%d",&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]; ///不选该物品
for(int k=1;k*v[i]<=j;k++) ///选择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;
}
```
```
///一维
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],f[N];
int n,m;
int main(){
cin >>n >>m;
for(int i=1;i<=n;i++) scanf("%d%d",&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.多重背包
a.题目描述
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
b.代码实现
```
///直接拆分法
#include<iostream>
#include<cstdio>
using namespace std;
const int N=110;
int f[N];
int n,m;
int main(){
cin >>n >>m;
while(n--){
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int k=1;k<=s;k++){///将这s个同样的物品单独进行选择
for(int j=m;j>=v;j--){
f[j]=max(f[j],f[j-v]+w);
}
}
}
cout <<f[m] <<endl;
return 0;
}
```
```
///二进制拆分法
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N=11000;
int f[N];
int n,m;
class Good{
public:
int v,w;
};
int main(){
cin >>n >>m;
vector<Good> goods;///goods({int,int})
while(n--){
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
///二进制拆分物品
for(int k=1;k<=s;k<<=1){ ///k为2的整次幂
s-=k; ///当前物品个数减去2的整次幂
goods.push_back({k*v,k*w});
}
if(s>0) goods.push_back({s*v,s*w}); ///将剩余物品加入goods中
}
for(auto x:goods){ ///将每个物品循环
for(int j=m;j>=x.v;j--){ ///尝试选择当前物品x
f[j]=max(f[j],f[j-x.v]+x.w);
}
}
cout <<f[m] <<endl;
return 0;
}
```
4.分组背包
a.题目描述
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
- 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
- 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
b.代码实现
#include<iostream>
using namespace std;
const int N=110;
int f[N],v[N],w[N];
int n,m;
int main(){
cin >>n >>m;
while(n--){
int s;
cin >>s;
for(int i=0;i<s;i++) cin >>v[i] >>w[i];
for(int j=m;j>=0;j--){ ///因为每个背包只能选择一个物品,故要放在外层
for(int k=0;k<s;k++){
if(j>=v[k]) f[j]=max(f[j],f[j-v[k]]+w[k]);
}
}
}
cout <<f[m] <<endl;
return 0;
}