原题链接:http://poj.org/problem?id=3624
经典01背包问题,先想到用动规解此题,递推式可以很轻易的得出:
dp[i][j]为从前j种物品里选出重量不大于i的物品的最大价值。
dp[i][j]=max(dp[i][j-1]+dp[i-w[j]][j-1]+d[j])
代码如下
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[13000][3500];//dp[i][j]=max(dp[i][j-1],dp[i-w[j]][j-1]+d[j])
int n, m, w[3500], d[3500];
int ans = 0;
int main()
{
cin >> n >> m;
memset(d, 0, sizeof(d));
memset(w, 0, sizeof(w));
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
cin >> w[i] >> d[i];
}
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if(i-w[j]>=0)
dp[i][j] = max(dp[i][j - 1], dp[i - w[j]][j - 1] + d[j]);
else {
dp[i][j] = dp[i][j - 1];
}
if (dp[i][j] > ans) {
ans = dp[i][j];
}
}
}
cout << ans;
return 0;
}
不过这个二维数组过大会爆内存,那怎么办呢,于是思考能不能用一位数组解决问题,发现是可以的。
因为求解每个dp[i][j]只会用到他左边一列的值dp[i][j-1]以及左一列上边个元素dp[i-w[j]][j-1]。而dp[i][j-1]最多会被使用两次,一次是被他右边的元素dp[i][j],一次是被他右下角某个元素dp[i+w[j]][j]使用因此只要保证从下到上更新dp[i][j]那么dp[i][j-1]一定已经被使用过两次没用了,当得到dp[i][j]的时候可以覆盖掉dp[i][j-1],每次都这么覆盖则不需要列。因此一维数组就可以解决问题。
代码如下(这下成功AC了)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[13000];
int n, m, w[3500], d[3500];
int ans = 0;
int main()
{
cin >> n >> m;
memset(d, 0, sizeof(d));
memset(w, 0, sizeof(w));
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
cin >> w[i] >> d[i];
}
for (int i = 1; i <= n;i++) {
for (int j = m; j >0; j--) {
if(j-w[i]>=0)
dp[j] = max(dp[j], dp[j - w[i]] + d[i]);//dp递推方程
if (ans < dp[j])
ans = dp[j];
}
}
cout << ans;
return 0;
}
刚好最近在上算法分析与设计对01背包这个问题讲了几种方法
回溯法+减枝解决
找是否能装入这个物品也就是找这些物品的01序列就好了,简化为找子集问题,然后如果一个序列有可能含有最优解就继续递归找其子集否则剪枝。
什么时候有最优解捏?两个限制条件一个是重量,只要已装物品重量没有超过背包容量则可以继续装下去,第二是当装了这个物品后,先扫描其后面序列有可能出现最优解才递归下去,否则就剪枝。
代码如下
#include<iostream>
#include<map>
#include<cstring>
#include<algorithm>
using namespace std;
int n, m;
int bestp = 0;
struct OBJ {
int p;
int w;
}obj[3500];
int flag[3500];
int Constraint(int idx) {
int cw = 0;
for (int i = 1; i <= idx; i++) {
if (flag[i] == 1) {
cw += obj[i].w;
}
}
return cw;
}
int Bound(int idx) {
int cp=0;
for (int i = 1; i <idx; i++) {
if (flag[i] == 1) {
cp += obj[i].p;
}
}
int rw = m - Constraint(idx-1);//剩余重量
while (idx <= n ) {//从index后一个个找是否满足价值的
if (rw - obj[idx].w >= 0) {
rw -= obj[idx].w;
cp += obj[idx].p;
}
idx++;
}
return cp;
}
void Backtrack(int idx) {
if (idx > n) {
bestp = Bound(idx);
return;
}
else {
for (int i = 0; i <= 1; i++) {
flag[idx] = i;
if(Constraint(idx)<=m && Bound(idx + 1)>bestp)
Backtrack(idx + 1);
}
}
}
int main()
{
int sump = 0, sumw = 0;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> obj[i].w >> obj[i].p;
sump += obj[i].p;
sumw += obj[i].w;
}
if (sumw <= m)
bestp = sump;
else {
Backtrack(1);
}
cout << bestp;
return 0;
}
不过如果这样解的话会超时,在老师讲解下意识到只能优化剪枝方法了,上述代码中找上界是挨个尝试是否能放下这个物品如果能算其最大价值,要是能尽量放入最大的就好了。即找到最快拿到最优解的方法
对于Constraint的优化,在搜索其解空间树时,只要其左儿子是一个可行节点,也就是cw+w[i]<m满足背包重量要求则进入左子树,试想一下最大价值不会出现在背包还能装东西的时候,所以只有当背包不能装下东西的时候,即其右子树有可能包含最优解的时候进入右子树计算价值就好,否则将其右子树减去。
对于Bound函数的优化我们可以按物品单位价值的递减顺序对物品排列,然后按序将物品装入背包,举个例子:
输入;
4 7
3 9
5 10
2 7
1 4
这种情况下按物品单位重量价值递减排序为(4,3.5,3,2);
我们先装入物品4再装入物品3再装入物品1,此时背包剩余重量只剩1,价值为20,所以只能装入0.2的物品2,可以得到最优解上界为22。所以对于这个例子最优值不超过22。
算法代码如下
#include<iostream>
#include<map>
#include<cstring>
#include<algorithm>
using namespace std;
int n, m;
int cw = 0;//当前重量和当前的价值
float bestp = 0, cp = 0;
struct OBJ {
int p;
int w;
}obj[3500];
bool cmp(OBJ a, OBJ b) {
float ap = a.p * 1.0 / a.w * 1.0;
float bp = b.p * 1.0 / b.w * 1.0;
return ap > bp;
}
float Bound(int i) {
float t = cp;
int rw = m - cw;
while (i <= n && obj[i].w <= rw) {
rw -= obj[i].w;
t += obj[i].p;
i++;
}
if (i <= n) {
t += obj[i].p*1.0 / obj[i].w * rw;//算最优解上界
}
return t;
}
void Backtrack(int i) {
if (i > n) {
bestp = cp;
return;
}
else {
if (cw + obj[i].w <= m) {//只要可行就进入左孩子节点
cw += obj[i].w;
cp += obj[i].p;
Backtrack(i + 1);
cw -= obj[i].w;
cp -= obj[i].p;
}
if (Bound(i) > bestp) {//有可能有最优解才进入右子树
Backtrack(i+1);
}
}
}
int main()
{
float sump = 0, sumw = 0;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> obj[i].w >> obj[i].p;
sump += obj[i].p;
sumw += obj[i].w;
}
if (sumw <= m)
bestp = sump;
else {
sort(obj + 1, obj + 1 + n, cmp);
Backtrack(1);
}
cout << bestp;
return 0;
}
计算上界需要O(n),最坏情况下需要找2^n次(叶子节点个数)上界所以时间复杂度为O(n*2^n)
dp和回溯总和来看的话显然dp时间复杂度低,代码量也小,不过回溯法更易于理解一点把