01背包问题
问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i
件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000 0<vi,wi≤1000
题解
题解(1)二维空间
(1)定义状态: f [ i ] [ j ] f[i][j] f[i][j]:前 i i i件物品,在容量为 j j j下的最优解(最大价值)
解释:当前状态 i i i依赖于上一个状态 i − 1 i-1 i−1,,即在f[0][0]时开始,有N件物品,则需要N次决 策,每一次对第 i 件物品的决策,状态f[i][j]不断由之前的状态更新而来。
(2)状态转移方程:当背包容量足够时可以选择装入第 i 件物品或者不装。
装入:
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
−
w
[
i
]
+
v
[
i
]
f[i][j] = f[i-1][j - w[i] + v[i]
f[i][j]=f[i−1][j−w[i]+v[i]
不装:
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
]
f[i][j] = f[i-1][j]
f[i][j]=f[i−1][j]
因此状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
{
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
w
[
i
]
+
v
[
i
]
}
f[i][j] = max\{f[i-1][j], f[i-1][j - w[i] + v[i]\}
f[i][j]=max{f[i−1][j],f[i−1][j−w[i]+v[i]}
(3)边界条件
当背包容量不足时:
j
<
w
[
i
]
j < w[i]
j<w[i],此时下一状态前 i 个物品最优解即为前 i−1 个物品最优解。
当
前
状
态
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
]
当前状态f[i][j] = f[i-1][j]
当前状态f[i][j]=f[i−1][j]
代码:
#include <iostream>
using namespace std;
const int N = 1010;
int V[N], W[N];
int f[N][N];
//int f[N];
int n, w;
int main(void)
{
cin >> n >> w;
for(int i = 1; i <= n; i++)
{
cin >> W[i] >> V[i];
}
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= w; j++)
{
if(W[i] > j)
f[i][j] = f[i-1][j];
else if(W[i] <= j)
f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
}
}
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= v; j++)
cout << f[i][j] << " ";
cout << endl;
}
/*for(int j = 0; j <= w; j++)
cout << f[j] << " ";*/
cout << endl;
cout << f[n][w] << endl;
system("pause");
return 0;
}
题解(2)状态压缩至一维
将二维状态
f
[
i
]
[
j
]
f[i][j]
f[i][j]压缩至一维
f
[
j
]
f[j]
f[j],此时少了维度 i ,之所以可以压缩状态 i 是因为每一次的第 i 个状态只与上一状态 i - 1 有关,与其他无关,因此可以对物品维度进行压缩,直接在 N 件物品上寻找最优值。
(1)定义状态:
f
[
j
]
f[j]
f[j]:N 件物品,在背包容量为 j 时的最优解
注意1:对背包容量 j 进行遍历时要从 w (最大容量)开始,然后递次倒序遍历。
注意2之所以要倒序遍历,是因为在二维情况下,状态
f
[
i
]
[
j
]
f[i][j]
f[i][j]是由上一轮
i
−
1
i - 1
i−1的状态得来的,
f
[
i
]
[
j
]
f[i][j]
f[i][j]与
f
[
i
−
1
]
[
j
]
f[i - 1][j]
f[i−1][j]是独立的。而优化到一维后,如果我们还是正序,则有
f
[
较
小
体
积
]
f[较小体积]
f[较小体积]更新到
f
[
较
大
体
积
]
f[较大体积]
f[较大体积],则有可能本应该用第
i
−
1
i-1
i−1轮的状态却用的是第 i 轮的状态。
(2)状态转移方程
f
[
j
]
=
m
a
x
{
f
[
j
]
,
f
[
j
−
w
[
i
]
]
+
v
[
i
]
}
f[j] = max\{f[j], f[j - w[i]] + v[i]\}
f[j]=max{f[j],f[j−w[i]]+v[i]}
代码:
#include <iostream>
using namespace std;
const int N = 1010;
int V[N], W[N];
//int f[N][N];
int f[N];
int n, w;
int main(void)
{
cin >> n >> w;
for(int i = 1; i <= n; i++)
{
cin >> W[i] >> V[i];
}
for(int i = 1; i <= n; i++)
{
for(int j = w; j >= W[i]; j--)
{
if(W[i] > j)
//f[i][j] = f[i-1][j];
f[j] = f[j];
else if(W[i] <= j)
//f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
f[j] = max(f[j], f[j - W[i]] + V[i]);
}
}
/*for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= v; j++)
cout << f[i][j] << " ";
cout << endl;
}*/
for(int j = 0; j <= w; j++)
cout << f[j] << " ";
cout << endl;
cout << f[w] << endl;
system("pause");
return 0;
}
求解在恰好装满背包的情况下具体装入方案
为求解恰好装满背包时取得的最大价值,这里需要引入有效状态和无效状态,我们最开始时初始化转移矩阵全为0,这个初始化的方法意思是:对任意的一个状态,把当前的价值初始化为0,代表背包为空时所包含的物体价值为0 。即认为不管背包当前容量,背包是空的那么价值就是0,,没装满背包的状态都是有效的状态,我们称之为有效状态。
当要求恰好装满时,我们就不可以这么做了,因为我们认为背包没恰好装满的话,当前背包的状态无效,即无效状态,只有恰好装满时候才是有效状态。
我们不妨定义当背包的状态为无效状态时,f[i]的值是负无穷,这样我们就可以从状态转移矩阵中区分出有效状态和无效状态了。
先不关心如何计算f,有了这个定义,对于恰好装满的01背包问题,我们只需要判断f[V]中的值是否是负无穷,就知道背包能否装满了。当f[V]中的值不是负无穷,我们接下来需要设计状态转移方程,使f[V]就是背包恰好装满时候的最大价值。
这里参考了另一篇博客,那里有推到有效状态和无效状态的证明
https://blog.csdn.net/Iseno_V/article/details/100697105
这里只介绍如何解决问题
也就是说在初始化转移矩阵上下文章,即f[0] = 0,else f[i] = -INF,这里的意思是说智能从f[0]开始算有效状态,其他均为无效状态。恰好把问题转化成背包容量为0时,装的物体体积为0是有效状态。
for(int i = 0; i < N; i++)
f[i] = -100000000;
f[0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = w; j >= W[i]; j--)
{
//f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
if(f[j] < f[j - W[i]] + V[i])
{
path[i][j] = 1;
f[j] = f[j - W[i]] + V[i];
}
if(f[j] < 0)
f[j] = -100000000;
}
}
下面介绍如何求解被选物品
(1)定义
需要定义一个路径矩阵path[N][N],表示有n(物品数)行m(背包重量)列,在进行状态转移时,若
f
[
j
]
<
f
[
j
−
W
[
i
]
]
+
V
[
i
]
f[j] < f[j - W[i]] + V[i]
f[j]<f[j−W[i]]+V[i]则在路径矩阵中赋值 1 ,其他为 0 ,这样求解出物品矩阵了
(2)输出
对背包物品个数 n 进行倒序循环,若path[i][w]为 1 ,则输出 W[i],并把当前背包容量减去W[i],即w - W[i],代码如下
int p = w;
for(int i = n; i >= 0; i--)//路径输出
{
if(w < 0) break;
if(path[i][p])
{
cout << W[i] << ' ';
p -= W[i];
}
}
完整代码
#include <iostream>
using namespace std;
const int N = 1010;
int V[N], W[N];
//int f[N][N];
int f[N];
int n, w;
int path[N][N];
int main(void)
{
cin >> n >> w;
for(int i = 1; i <= n; i++)
{
cin >> W[i] >> V[i];
}
for(int i = 0; i < N; i++)
f[i] = -100000000;
f[0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = w; j >= W[i]; j--)
{
//f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
if(f[j] < f[j - W[i]] + V[i])
{
path[i][j] = 1;
f[j] = f[j - W[i]] + V[i];
}
if(f[j] < 0)
f[j] = -100000000;
}
}
/*for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= v; j++)
cout << f[i][j] << " ";
cout << endl;
}*/
// for(int j = 0; j <= w; j++)
// cout << f[j] << " ";
// cout << endl;
// cout << f[w] << endl;
int p = w;
for(int i = n; i >= 0; i--)//路径输出
{
if(w < 0) break;
if(path[i][p])
{
cout << W[i] << ' ';
p -= W[i];
}
}
cout << endl;
if (f[w] > 0)
{
cout << f[w] << endl;//背包恰好装满了,输出结果
}
else
{
cout << "error" << endl;//背包不能恰好装满
}
system("pause");
return 0;
}