前言
本章开始写背包问题,具体就贴算法了,后面遇到比较特殊的题的会继续更新,这次更新就针对经典例题写一写过程和答案。
一、0-1背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次
。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式 :
- 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
- 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式: 输出一个整数,表示最大价值。
数据范围: 0<N,V≤1000 // 0<vi,wi≤1000
#include <vector>
#include <iostream>
using namespace std;
void fun1(vector<int>& v, vector<int>& w, vector<vector<int>>& dp, int n, int m){
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
//这一行必须加上,因为有可能背包装不下,存在 j<v[i-1]的情况
//状态转移方程,不选取第i个物品
dp[i][j] = dp[i - 1][j];
if(j>=v[i-1]){
//选取第i个物品和不选取第i个物品的最大值
dp[i][j]=max(dp[i-1][j-v[i-1]] + w[i-1],dp[i-1][j]);
}
}
}
cout<<dp[n][m]<<endl;
}
void fun2(vector<int>& v, vector<int>& w, vector<int>& f, int n, int m){
//fun1 中的更新步骤 依赖于上一轮的结果
//我们使用下面的方法,逆序更新,
//f[8]=max(f[3],f[3-v[0]]+w[0]) f[3] 和 f[3-v[0]] 就是上一轮的结果
for(int i=0;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;
}
int main(){
int n,m;
cin>>n>>m;
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
vector<int> f(m+1,0);
vector<int> v(n,0);
vector<int> w(n,0);
for(int i=0;i<n;i++) cin>>v[i]>>w[i];
// fun1(v,w,dp,n,m);
fun2(v,w,f,n,m);
return 0;
}
二、完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用
。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。附原文链接
输入格式 :
- 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
- 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别>表示第 i 种物品的体积和价值。
输出格式:输出一个整数,表示最大价值。
数据范围:0<N,V≤1000 0<vi,wi≤1000
#include <vector>
#include <iostream>
using namespace std;
void fun1(vector<int>& v, vector<int>& w, vector<vector<int>>& dp, int n, int m){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k*v[i-1]<=j;k++){
//因为 完全背包问题 不限制个数,所以需要在 k*v[i] 小于 j 的情况下也要考虑多选的情况。
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i-1]]+k*w[i-1]);
}
}
}
cout<<dp[n][m]<<endl;
}
void fun2(vector<int>& v, vector<int>& w, vector<int>& f, int n, int m){
// dp[i][j] = max(dp[i-1][j], dp[i-1,j-v]+w , dp[i-1,j-2*v]+2*w,... dp[i-1,j-k*v]+k*w)
// dp[i][j-v] = max( dp[i-1][j-v], dp[i-1][j-2*v]+*w,... dp[i-1,j-k*v]+(k-1)*w)
//两个公式合并后的 dp[i][j] = max(dp[i-1][j], dp[i][j-v]+w);
//当不选第i个物品时,dp[i][j]=dp[i-1][j], 所以上面的公式有可以更新成 dp[i][j] = max(dp[i][j], dp[i][j-v]+w);
//i 都是当前迭代的信息,从正序读就可以正确读取信息
for(int i=0;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;
}
int main(){
int n,m;
cin>>n>>m;
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
vector<int> f(m+1,0);
vector<int> v(n,0);
vector<int> w(n,0);
for(int i=0;i<n;i++) cin>>v[i]>>w[i];
//fun1(v,w,dp,n,m);
fun2(v,w,f,n,m);
return 0;
}
三、多重背包问题
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
输入格式
- 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
- 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式 输出一个整数,表示最大价值。
第一种问题:
- 数据范围 0<N,V≤100, 0<vi,wi,si≤100(普通方法)
- 数据范围 0<N,V≤2000, 0<vi,wi,si≤2000(二进制法)
- 数据范围 0<N,V≤20000, 0<vi,wi,si≤20000(单调队列法)
关于单调队列法,我把求解过程写一下,具体可以参考大佬些写的原文链接,我在代码中,加了注释。
不放第 i 个物品 i = dp[i-1][j] 和 放k个第 i 个物品 i = dp[i-1][j - k*v] + k*w
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v] + w, dp[i-1][j-2*v] + 2*w,..., dp[i-1][j-k*v] + k*w)
可以重复利用dp数组来保存上一轮的信息
dp[j] = max(dp[j], dp[j-v] + w, dp[j-2*v] + 2*w, dp[j-3*v] + 3*w, ...)
接下来,我们把 dp[0] --> dp[m] 写成下面这种形式
dp[0], dp[v], dp[2*v], dp[3*v], ... , dp[k*v]
dp[1], dp[v+1], dp[2*v+1], dp[3*v+1], ... , dp[k*v+1]
dp[2], dp[v+2], dp[2*v+2], dp[3*v+2], ... , dp[k*v+2]
...
dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j]
m 一定等于 k*v + j,其中 0 <= j < v
dp[k*v+j] 只依赖于 { dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] }
因为我们需要的是{ dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] } 中的最大值,
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 j 个单调队列的问题
所以,我们可以得到
dp[j] = dp[j]
dp[j+v] = max(dp[j] + w, dp[j+v])
dp[j+2v] = max(dp[j] + 2w, dp[j+v] + w, dp[j+2v])
dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
对上面的式子进行变形得:
dp[j] = dp[j]
dp[j+v] = max(dp[j], dp[j+v] - w) + w
dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
这样,每次入队的值是 dp[j+k*v] - k*w
单调队列问题,最重要的两点
1)维护队列元素的个数,如果不能继续入队,弹出队头元素
2)维护队列的单调性,即:尾值 >= dp[j + k*v] - k*w
#include <vector>
#include <iostream>
using namespace std;
//暴力法
void fun1(){
int n,m;
cin>>n>>m;
vector<int> dp(m+1,0);
for (int i = 0; i < n; i++)
{
int v,w,s;
cin>>v>>w>>s;
for(int j=m;j>=v;j--){
for(int k=0; k<=s && k*v<=j;k++){
dp[j]=max(dp[j],dp[j-k*v]+k*w);
}
}
}
cout<< dp[m]<<endl;
return 0;
}
//二进制法
void fun2(){
int n,m;
cin>>n>>m;
vector<pair<int,int>> goods;
vector<int> dp(m+1,0);
for (int i = 0; i < n; i++)
{
int v,w,s;
cin>>v>>w>>s;
//把同一个商品给拆分成不同的个数,重组成新的“商品”,转换成0-1背包问题
for(int k=1;k<=s;k*=2){
s-=k;
goods.push_back({k*v,k*w});
}
if(s>0) goods.push_back({s*v,s*w});
}
for(auto t:goods){
for(int j=m;j>=t.first;j--){
dp[j]=max(dp[j],dp[j-t.first]+t.second);
}
}
cout<<dp[m]<<endl;
return 0;
}
//单调队列法
int fun3(){
int n,m;
cin>>n>>m;
vector<int> dp(m+1,0);
vector<int> pre(m+1,0);
//因为每个物品的上限是20000个
//q队列保证每个物品被选择的次数范围是(0-s),所以数组范围是s+1
vector<int> q(20001,0);
for(int i=0;i<n;i++){
pre=dp;
//copy(dp.begin(),dp.end(),pre.begin());
int v,w,s;
cin>>v>>w>>s;
for(int j=0;j<v;j++){
// 对于每一个j,维护一个队列,重新定义head和tail可以重用之前的值
int head=0,tail=-1;
for(int k=j;k<=m;k+=v){
// 每个队列保存 s+1 个元素,当超出范围时,把无用的元素踢出,
// 需要注意一点,q保存的仅仅是 k*v + j, 位于head元素的值 已经在 dp 数组中更新
if(head<=tail && k-s*v>q[head]) head++;
// pre数组存储的上一轮dp的值,pre数组将 本轮最大的值 存在队列的前面位置,即保证pre数组是单调递减的
// dp数组实际存储的 dp[j+k*v],所以在 while 比较的时候会减去 k*w
while (head<=tail && pre[q[tail]] - (q[tail]-j)/v*w <= pre[k]-(k-j)/v*w) tail--;
if(head <= tail) dp[k] = max(dp[k],pre[q[head]]+(k-q[head])/v*w);
q[++tail]=k;
}
}
}
cout<<dp[m]<<endl;
return 0;
}
四、混合背包问题
有 N 种物品和一个容量是 V 的背包。物品一共有三类
:
- 第一类物品只能用1次(01背包);
- 第二类物品可以用无限次(完全背包);
- 第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
- si=−1 表示第 i 种物品只能用1次;
- si=0 表示第 i 种物品可以用无限次;
- si>0 表示第 i 种物品可以使用 si 次;
输出格式: 输出一个整数,表示最大价值。
数据范围 0<N,V≤1000 0<vi,wi≤1000 −1≤si≤1000
#include <vector>
#include <iostream>
using namespace std;
struct good{
int kind;
int v,w;
};
// 如果是0-1背包 和 多重背包 就用二进制法 转换成 0-1 背包
// 如果是完全背包,就用完全背包的方法解决
// 定义一个结构体,kind=-1 代表 0-1背包,king = 0 代表完全背包
int main(){
int n,m;
cin>>n>>m;
vector<int> dp(m+1,0);
vector<good> goods;
for(int i=0;i<n;i++){
int v,w,s;
cin>>v>>w>>s;
if(s<0){
goos.push_back({-1,v,w});
}else if(s==0){
goos.push_back({0,v,w});
}else{
for(int k=1;k<s;k*=2){
s-=k;
goods.push_back({-1,k*v,k*w});
}
if(s>0) goods.push_back({-1,s*v,s*w});
}
}
for(auto s:goods){
if(s.kind==-1){
for(int j=m;j>=s.v;j--){
dp[j]=max(dp[j],dp[j-s.v]+s.w);
}
}else if(s.kind==0){
for(int j=s.v;j<=m;j++){
dp[j]=max(dp[j],dp[j-s.v]+s.w);
}
}
}
cout<<dp[m];
return 0;
}
五、二维费用背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M
。每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
输入格式
- 第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
- 接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。
输出格式: 输出一个整数,表示最大价值。
数据范围: 0<N≤1000 0<V, M≤100 0<vi,mi≤100 0<wi≤1000
#include <vector>
#include <iostream>
using namespace std;
int main(){
int n,v,w;
cin>>n>>v>>w;
vector<vector<int>> dp(v+1,vector<int>(w+1,0));
//多了一层循环,来装第二维的费用
for(int i=0;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
for(int j=v;j>=a;j--){
for(int k=w;k>=b;k--){
dp[j][k]=max(dp[j][k],dp[j-a][k-b]+c);
}
}
}
cout<<dp[v][w]<<endl;
return 0;
}
六、分组背包问题
有 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
#include <vector>
#include <iostream>
using namespace std;
//方法很简单
//第一步,在0-1背包基础上,加一轮循环,输入第i组的信息
//第二步,加一轮循环,在第i组选一个物品
int main(){
int n,m;
cin>>n>>m;
vector<int> dp(m+1,0);
vector<int> V(101,0);
vector<int> W(101,0);
for(int i=0;i<n;i++){
int s;
cin>>s;
for(int j=0;j<s;j++) cin>>V[j]>>W[j];
// j-- 注意一下
for(int j=m;j>=0;j--){
for(int k=0;k<s;k++)
// 当遇到最大的值时,后保留最大值
// 最大值只有一个,在这一组中也只会选一个物品
// 这里注意 j>=V[k]
if(j>=V[k]) dp[j]=max(dp[j],dp[j-V[k]]+W[k]);
}
}
cout<<dp[m]<<endl;
return 0;
}
七、有依赖的背包问题
有 N 个物品和一个容量是 V 的背包。物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。如下图:
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
输入格式
- 第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
- 接下来有 N 行数据,每行数据表示一个物品。 第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。
输出格式 :输出一个整数,表示最大价值。
数据范围 :1≤N,V≤100 , 1≤vi,wi≤100
父节点编号范围:内部结点:1≤pi≤N;根节点 pi=−1;
这个题还是比较复杂的,但是如果搞清楚了树形dp和,动态规划的核心就可以了。还有一个更精简的我写在下面。
#include <vector>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=110;
int n,m;
//存储物品的体积和价值,以及动态规划的数组
int v[N],w[N],f[N][N];
//数组e是边的集合,下标是边的序号,值是边的终点
//数组ne是某一节点的边的集合,下标是边的序号,值是某一节点的上一条边的序号
//数组h是节点的集合,下标是节点的序号,值是最后一条边的序号
//idx是边的序号
//对应关系:通过 h[p]—> 找到父节点最后一条边 idx-> ne[idx] 找到父节点的上一条边 id -> e[id] 找到父节点的孩子节点
int e[N],ne[N],h[N],idx;
void add(int p,int i){
e[idx]=i;ne[idx]=h[p];h[p]=idx++;
}
void dfs(int u){
for(int i=h[u];i!=-1;i=ne[i]){
int son=e[i];
dfs(son);
//选择子节点就必须选择父节点,所以这里要将父节点的体积减去
for(int j=m-v[u];j>=0;j--){
//孩子节点的价值
for(int k=0;k<=j;k++){
//不选择孩子节点,自己的总价值就不会变
//选择孩子节点,体积就会减少k,同时要加上孩子节点,体积为k是的价值
f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
}
}
}
//下面两个循环就是选择第u个物品,还是不选第u个物品
//上面的循环只是选择还是不选孩子节点
for(int i=m;i>=v[u];i--){
//当体积大于v[u],要将第u个物品的价值也加上
f[u][i]=f[u][i-v[u]]+w[u];
}
for(int i=0;i<v[u];i++){
//当体积小于v[u],放不下第u个物品,所以要置为0
f[u][i]=0;
}
}
int main(){
memset(h,-1,sizeof(h));
cin>>n>>m;
int root;
for(int i=1;i<=n;i++){
int p;
cin>>v[i]>>w[i]>>p;
if(p==-1){
root=i;
}else{
add(p,i);
}
}
dfs(root);
cout<<f[root][m]<<endl;
return 0;
}
第二种遍历的写法,这种还是比较简洁的,但是有一点要说明一下,之前定义的树的邻接表过于复杂,大家可以直接建立一个二维数组来表示。我就不写了。原链接我给大家放这里:原链接
int dfs_1(int u){
//当背包大于v[u]时,应该把第u个物品放进去
for(int i=m;i>=v[u];i--) f[u][i]=w[u];
// 这个地方一定要注意 i=ne[i];上一条边,不用写成 i=ne[u],我老是写错……
for(int i=h[u];i!=-1;i=ne[i]){
int son=e[i];
dfs_1(son);
//子树和父节点 总的空间应该是v[u]~m
//v[u]是分给父节点的
// j>=v[u]
for(int j=m;j>=v[u];j--){
//子树的空间就是j-v[u]
for(int k=0;k<=j-v[u];k++){
f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
}
}
}
}
八、背包问题求方案数
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。
输入格式
- 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
- 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式 输出一个整数,表示 方案数 模 10^9+7 的结果。
数据范围 0<N,V≤1000 0<vi,wi≤1000
首先我们要明确一点动态规划的数组初始化有一定的技巧:
- 全部初始化为0,那么
dp 数组
代表当体积小于或者等于 m 时,最大价值是多少。
因为数组 dp 全部为0,假设 dp[k] 取得最大值,那么 dp[m] 可以由 dp[m-k] 转移过来。dp[m] 处就是最大值。
f[j]=max(f[j],f[j-v[i]]+w[i]);
- dp[0] 初始化为0,其余全部初始化为负无穷,那么 dp[m] 只能从 dp[0] 转移过来,
dp 数组
含义就代表当体积恰好为m时,最大价值是多少。dp[m] 处不一定是最大值。
因为 dp[m] 不一定会发生状态转移,其值可能是无穷小。
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1010, MOD=1000000007, INF=1000000;
int n,m;
int f[N],g[N];
int main(){
for(int i=1;i<=m;i++) f[i]=-INF;
g[0]=1;
cin>>n>>m;
for(int i=0;i<n;i++){
int v,w;
cin>>v>>w;
for(int j=m;j>=v;j--){
int t=max(f[j],f[j-v]+w);
// 多一步,找到中间取最大值的途径,并记录下来
int s=0;
if(t==f[j]) s+=g[j];
if(t==f[j-v]+w) s+=g[j-v];
if(s>MOD) s%=MOD;
f[j]=t;
g[j]=s;
}
}
int maxn=0;
// 找到最大值
for(int i=0;i<=m;i++) maxn=max(maxn,f[i]);
int res=0;
// 将所有可以取到最大值的途径相加
for(int i=0;i<=m;i++){
if(maxn == f[i]){
res+=g[i];
if(res>MOD) res%=MOD;
}
}
cout<<res<<endl;
return 0;
}
九、背包问题的方案
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。
物品的编号范围是 1…N。输入格式
- 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
- 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
- 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。
数据范围 0<N,V≤1000 0<vi,wi≤1000
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1010;
int f[N][N];
int V[N],W[N];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>V[i]>>W[i];
//之前的f[i][j] 代表背包体积为j时,前i个物品的总价值
//为了从第一个物品开始遍历
//我们换一下顺序,令f[i][j]代表背包体积为j时,第i个物品到最后一个物品的总价值
for(int i=n;i>=1;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]);
}
}
int vol=m;
// 如果 f[i][vol] 由 f[i+1][vol-V[i]]+W[i] 状态转换过来
// 说明第i个物品被选择了。
for(int i=1;i<=n;i++){
// 要加上 vol>=V[i] 的判断,避免越界
if( vol >= V[i] && f[i][vol]==f[i+1][vol-V[i]]+W[i]){
cout<<i<<" ";
vol-=V[i];
}
}
return 0;
}
总结
背包问题就写完了,有问题的欢迎留言,看的B站背包九讲,以及这个网站的练习。