一、01背包问题:
有一个最大容量为\(v\)的背包,有一系列物品,每一个物品都有体积\(v_i\)和价值\(w_i\)两个属性,要求选出一部分物品(每个物品只能选一次),在背包能装得下它们的前提下使其价值之和最大。
要解决这个问题,需要使用动态规划的方法。
二、动态规划的最基本内容:
动态规划的最基本内容包含状态的表示和计算:
1.状态的表示:
状态的表示有两个组成部分:状态表示的集合和状态表示的性质。例如,在01背包问题中,我们可以使用\(f\left ( i,j \right )\)来表示从前i个物品中选出体积不超过j的所有组合的价值最大值,其中:“从前i个物品中选出体积不超过j的所有组合”就是状态表示的集合;“价值最大值”就是状态表示的性质。
2.状态的计算:
同样是01背包问题(假设问题给出的条件是i个物品,限制体积是j),为了计算出最终所需的结果\(f\left ( i,j \right )\),我们需要对集合进行划分(这样的划分一般要满足不重不缺的条件:同一种情况不能出现在不同的划分中;任何一种情况必须要有所属的划分)。
在此问题中,可以将\(f\left ( i,j \right )\)划分为:①不包含第i个物品的所有情况;②包含第i个物品的所有情况。经过分析,①可表示为\(f\left ( i-1,j \right )\),②可表示为\(f\left ( i-1,j-v_i \right )+w_i\),最终\(f\left ( i,j \right )\)的值就为max(①,②)。以此类推,从而可以由\(f\left ( 1,j_1 \right )\)逐步计算出结果。
三、代码:
要注意动态规划与递归的不同。
本问题也可以使用递归进行计算:编写一个函数\(f\left ( i,j \right )\),返回值是\(f\left ( i-1,j \right )\)和\(f\left ( i-1,j-v_i \right )+w_i\)中的较大者,只需要规定好\(f\left ( 1,j_1 \right )\)的一系列值,就可以在主函数中直接输出函数\(f\left ( i,j \right )\)的返回值。
代码如下(递归):
/*
输入:物品的数量 背包的容量
接下来分别输入每个物品的体积和价值
输出:最大价值
*/
#include <iostream>
using namespace std;
typedef pair<int,int> pii;
const int N = 1010;
pii thing[N];//从1开始,体积,价值
int n, v;
int f(int i, int j) {
if (i > 1) {
int x = max(f(i - 1, j), f(i - 1, j - thing[i].first) + thing[i].second);
return x;
}
else {
if (j < 0)return -99999;
else if (j < thing[1].first)return 0;
else return thing[1].second;
}
}
void solution() {
cin >> n >> v;
for (int i = 1; i <= n; i++) {
cin >> thing[i].first >> thing[i].second;
}
cout << f(n, v);
}
int main() {
int t = 1;
//cin >> t;
while (t--) {
solution();
}
}
在上文代码中,由于每一次函数调用都会进一步调用两次函数,所以当数据量较大的时候,就很容易导致运行超时。因此,可以从简单情况开始,逐步递推,最终得出所需结果。
代码如下(递推):
/*
输入:物品的数量 背包的容量
接下来分别输入每个物品的体积和价值
输出:最大价值
*/
#include <iostream>
using namespace std;
const int N = 1010;
pii thing[N];//从1开始,体积,价值
int n, v;
int f[N][N];
void solution() {
cin >> n >> v;
for (int i = 1; i <= n; i++) {
cin >> thing[i].first >> thing[i].second;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) {
if (j - thing[i].first >= 0)f[i][j] = max(f[i - 1][j], f[i - 1][j - thing[i].first] + thing[i].second);
else f[i][j] = f[i - 1][j];
}
}
cout << f[n][v];
}
int main() {
int t = 1;
//cin >> t;
while (t--) {
solution();
}
}
四、优化:
01背包问题中的状态表示是可以优化的:我们可以使用\(f(j)\)来表示体积不超过j的所有组合的价值最大值。
为什么可以这样优化呢?是因为每一次求\(f\left ( i,j \right )\)的值时,只需要访问\(f\left ( i-1,j \right )\)和\(f\left ( i-1,j-v_i \right )+w_i\),发现他们都是\(f(i-1, )\)层的数据,更早的数据已经失去意义,因此,我们可以舍弃\(f(i)\)这个维度,直接使用一维数组进行计算,省下大量空间。
代码如下:
/*
输入:物品的数量 背包的容量
接下来分别输入每个物品的体积和价值
输出:最大价值
*/
#include <iostream>
using namespace std;
const int N = 100010;
class Thing {
public:
int v, w;
};
Thing thing[N];//从1开始,体积,价值
int n, v;
int f[N];
void solution() {
cin >> n >> v;
int num = 0;
for (int i = 1; i <= n; i++) {
cin >> thing[i].v >> thing[i].w;
}
for (int i = 1; i <= n; i++) {
for (int j = v; j >= thing[i].v; j--) {
//为什么j要从v开始:为了不使新数据覆盖可能用到的旧数据
//j的结束点不是0:要访问f[j - thing[i].v],如果j<thing[i].v就无意义(也可加上一个判断)
f[j]=max(f[j], f[j - thing[i].v] + thing[i].w);
}
}
cout << f[v];
}
int main() {
int t = 1;
//cin >> t;
while (t--) {
solution();
}
}