前一段时间学习了01背包,现在自我总结一下。所谓背包问题,就是给你一个固定容量的容器,然后给你一堆价值与容量对应的物品。举个简单的例子,你有一个只能装 10公斤的背包,一天你你找到了一堆宝藏,这些宝藏有:2公斤价值5万元,3公斤价值6万元,3公斤价值5万元,4公斤价值1万元,6公斤价值8万元,5公斤价值7万元,你要尽可能装更高价值的物品,这里我们就需要用到01背包,一个简单的算法。
我们可以先预想一下,如果我们先放入价值大的,也就只能放入6公斤的一个和一个3公斤的宝藏,你一共只能够获得14万元,但是如果你按照性价比来选择的话,你就会选择2个3公斤的和一个2公斤的宝藏,一共价值16万元,但这是不是最优的呢?当然在接触这个算法前我的想法就是性价比最高,但是通过这个算法了解到只看重性价比是不能够得到最优解的,对数字敏感的人已经注意到最优解是选择一个2公斤的和一个3公斤价值高的那一个,还有一个就是选择5公斤的那一个,得到最优解18万元。接下来就来讲一讲这个算法的实现。
01背包的解决在我看来就是一个打表的一个过程。
正如上面这个表所示,上面的标号是当前装在背包中的重量,而侧面的标号为第几次放入物品。这个数据对应上面的例子中的数据,就跟着上面的数据讲了。首先将所有物品统计一下,按照这个方式排列(重量,价值),为了简便:2 5,3 6,3 5,4 1,6 8,5 7;拿第一行来说,首先放入2公斤,价值5万元的宝藏,所以在(1,2)(坐标)位置放入数字5,表示放入两公斤的东西,总共价值为5万元。然后第二行,接下来是3公斤价值6万元的宝藏,放入两公斤的时候只有5公斤,继承上一行的数据继续,然后放入3公斤的时候刚好现在的就是3公斤的宝藏,放入,在坐标(2,3)放入数字6,然后发现在放入了2公斤的前提下,放入3公斤,总共重量5公斤,所以可以在坐标(2,5)放入数字11,表示价值为11。然后下一行继续,不过每次到原来存有数字的地方的时候要进行判断取最大值。比如第3行,我们放3公斤价值5万元的宝藏,但是上一行数据是3公斤能够放入价值6万元的宝藏,这里显然就不要这个,直接继承上一行的数据,不用改变值,最后就可以得到最优解了。
接下来附上代码:点击打开链接 (以杭电的2602为例)反正是模板题。
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAXN 1005
int w[MAXN], v[MAXN], dp[MAXN][MAXN];//定义体积和价值的数组,以及要用到的dp数组
int main()
{
int T, n, i, j, sum;
scanf("%d", &T);
while (T--)
{
scanf("%d%d", &n, &sum);
for (i = 1; i <= n; i++)
{
scanf("%d", &v[i]);
}
for (i = 1; i <= n; i++)
{
scanf("%d", &w[i]);
}
memset(dp, 0, sizeof(dp));//先将dp数组初始化
for (i = 1; i <= n; i++)//i从1开始,从编号第一个的物品开始逐一放入
{
for (j = 0; j <= sum; j++)//j从零开始,表示当前放入的物品总重
{
if (j - w[i] >= 0 && dp[i - 1][j - w[i]] + v[i] > dp[i - 1][j])//因为我们每次放入都要去查询放入当前物品之前
//背包中已有物品的总价值,所以j必须得大于等于当前物品重量不然查询位置会越界,并且以此判断放入当前物品与以前相比是否价值增加
{
dp[i][j] = dp[i - 1][j - w[i]] + v[i];
}
else
{
dp[i][j] = dp[i - 1][j];//不满足上述条件时继承以前的数据
}
}
}
printf("%d\n", dp[n][sum]);//最后输入数据
}
return 0;
}
至于为什么j从零开始,我也有点疑惑,难道是有体积为0,但存在价值的东西?原谅我这里并没有想通,如果你恰巧看到了这,知道内情希望指点一下。
细心的人会发现,你怎么输出dp[n][sum]呢?和上面表格不同不是,好的下面我给出程序打出的表来看一下。
这是j从零开始的表,第一行为放入体积5价值1的东西,从5一直到10都放入了1,可以看成如果背包体积为5现在最多能够装的最大价值为1,以此类推。下一行就是体积为4,价值为2,也是从j等于4时开始类推,虽然和第一行一样要查询前一行之前的值,但是第一行可以忽略掉,毕竟前一行什么都没放不是。所以也是一样,当背包体积为4时可装下的最大价值,以此类推,然后加上判断当前价值与前一次当前体积的最大价值比较然后更新数据,最后就只有第n行第sum列的值最大,因为它对于每一次更新必定进行了一次比较,相当于每一行的第sum列是当前行中的最大值,而下一行的数字只可能比当前行大,不可能小,所以输出dp[n][sum]就可以了,不用遍历找出最大值。
然后给出上面举例的表:
然后给出一维的代码:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAXN 1005
int w[MAXN], v[MAXN], dp[MAXN];
int main()
{
int T, n, i, j, sum;
scanf("%d", &T);
while (T--)
{
scanf("%d%d", &n, &sum);
for (i = 1; i <= n; i++)
{
scanf("%d", &v[i]);
}
for (i = 1; i <= n; i++)
{
scanf("%d", &w[i]);
}
memset(dp, 0,sizeof(dp));
for (i = 1; i <= n; i++)
{
for (j = sum; j >= w[i]; j--)
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[sum]);
}
return 0;
}
上面是一维的代码,和刚才差不多的,之所以可以这样,从二维的代码就可以看出每次引用前一次的数据,但最后要用的只有最后一行,所以可以用一维,每一次更新,这样可以节省存储空间,代码也好写些,不过得注意j必须从sum开始,因为如果从也是从前面开始,就可能造成前面数据更新,后面数据更新时用的不是上一次更新的数据,而是这一行的更新数据。
不怎么理解推荐看着代码手推,这样会理解快一些。