01 背包问题

 1.1 题目

         有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。

1.2 基本思路

        这是最基础的背包问题,只需要考虑选取哪几个物品放入背包,总体积不超过V的前提下价值最大

        特点是:每种物品仅有一件,可以选择放或不放。

        若我们把取该物品记为1,不取该物品记为0,那么使用某种放入方式酱对应一个2进制串,因此这类问题也称为01背包问题。

         用子问题定义状态:即 F[i, v] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其状态转移方程便是:

        F[i, v] = max{F[i − 1, v], F[i − 1, V − Ci] + Wi}

        这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前 i 件物品放入容量为 V 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只和前 i − 1 件物品相关的问题。如果不放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入容量为 V的背包中”,价值为 F[i − 1, v];如果放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为 V − Ci 的背包中”,此时能获得的最大价值就是 F[i − 1, V − Ci] 再加上通过放入第 i 件物品获得的价值 Wi。
伪代码如下:

F[0, 0..V ] ← 0
for i ← 1 to N
    for v ← Ci to V
        F[i, v] ← max{F[i − 1, v], F[i − 1, v − Ci] + Wi}
```

2.1例题

hdu—2602 基础01问题

示例输入

1

5  10

1 2 3 4 5

5 4 3 2 1

样本输出

14

#include <queue>
#include <iostream>
#include <cstring>
#include <cmath>
#include <ctime>
#include <climits>
#include <vector>
#include <algorithm>
#include <map>
#include <cstdio>
#include <bits/stdc++.h>
#include <stack>

using namespace std;
typedef long long LL;
typedef unsigned long long ULL;


int T,N,V;
int v[11000];//商品的体积
int w[11000];//商品的价值
int f[11000][11000];//动态规划表

int main(){

    scanf("%d",&T);

    while(T--){

        scanf("%d%d",&N,&V);

        memset(f,0,sizeof(f));

        for(int i=1;i<=N;++i) cin>>w[i];
        for(int i=1;i<=N;++i) cin>>v[i];

        for (int i = 1; i <= N; i++){//动态规划
			for (int j = V; j >= 0; j--){
				if (j >= v[i]){//如果背包装得下当前的物体
					f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
				}
				else{//如果背包装不下当前物体
					f[i][j] = f[i - 1][j];
				}
			}
		}

        cout<<f[N][V]<<endl;

    }

    return 0;
}

 

2.2 背包问题最优解回溯

通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:

  • V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
  • V(i,j)=V(i-1,j-v(i))+w(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-v(i));
  • 一直遍历到i=1结束为止,所有解的组成都会找到。

就拿上面的例子来说吧:

  • 最优解为V(5,10)=14,而V(5,10)!=V(4,10)却有V(5,10) = V ( 4 , 10 - v(5) ) + w(5) = V(4,9) + 5 = 9 + 5 =14,所以第5件商品被选中,并且回到V(4,10-v(5))=V(4,9);
  • 有V(4,9)!=V(3,9)却有V(4,9) = V (3 , 10 - v(4) ) + w(4) =V(3,8) + 4 = 5 + 4 =9,所以第4件商品被选中,并且回到V(3,10-v(4))=V(3,8);
  • 有V(3,8)!=V(2,8)却有V(3,8) = V (2 , 10 - v(3) ) + w(3) =V(2,7) + 3 = 2 + 3 =5,所以第3件商品被选中,并且回到V(2,10-v(3))=V(2,7);
  • 有V(2,7)!=V(1,7)却有V(2,7) = V (1 , 10 - v(2) ) + w(2) =V(1,6) + 2 = 1 + 2 =3,所以第2件商品被选中,并且回到V1,10-v(2))=V(1,6);
  • 有V(1,6)!=V(0,6)=0,且不存在V(1,6) = V (0 , 10 - v(1) ) + w(1) ,所以第1件商品没被选择。

2.3 代码实现

#include <queue>
#include <iostream>
#include <cstring>
#include <cmath>
#include <ctime>
#include <climits>
#include <vector>
#include <algorithm>
#include <map>
#include <cstdio>
#include <bits/stdc++.h>
#include <stack>

using namespace std;
typedef long long LL;
typedef unsigned long long ULL;


int T,N,V;
int v[11000];//商品的体积
int w[11000];//商品的价值
int f[11000][11000];//动态规划表
int List[11000];//最优解情况


void Find(int i, int j) {//最优解情况
	if (i >= 1) {
		if (f[i][j] == f[i - 1][j]) {
			List[i] = 0;
			Find(i - 1, j);
		}
		else if (j - v[i] >= 0 && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
			List[i] = 1;
			Find(i - 1, j - v[i]);
		}
	}
}


int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&N,&V);
        memset(f,0,sizeof(f));
        for(int i=1;i<=N;++i) cin>>w[i];
        for(int i=1;i<=N;++i) cin>>v[i];

        for (int i = 1; i <= N; i++){//动态规划
			for (int j = V; j >= 0; j--){
				if (j >= v[i]){//如果背包装得下当前的物体
					f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
				}
				else{//如果背包装不下当前物体
					f[i][j] = f[i - 1][j];
				}
			}
		}

        cout<<f[N][V]<<endl;


        Find(N,V);
        for (int i = 1; i <=N; i++)			//最优解输出
            cout << List[i] << ' ';
        cout << endl;

    }

    return 0;
}

 3.1优化

由于我们用f[n][V]表示最大价值,但是当物品和背包容量比较大时,这种用法会占用大量的空间,那么我们是不是对此可以进一步优化呢?

由上面我们可以知道,我们必须要做出n次选择,所以外层n次循环是必不可少的,对于上面代码的内层循环,表示当第i个商品要放入容量为V的背包时所获得的价值,即先对子问题求解,这个也是必不可少的,所以时间复杂度为O(nV),这个已不能进一步优化,但是我们可以对空间进行优化。

for (int i = 1; i <= N; i++){//动态规划
	for (int j = V; j >= 0; j--){
		if (j >= v[i]){//如果背包装得下当前的物体
		    f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
		}
		else{//如果背包装不下当前物体
			f[i][j] = f[i - 1][j];
	    }
	}
}

 可以发现如下问题:
1)状态表f的遍历顺序为从第1行开始一行一行遍历,且在遍历第i行时候不会用到第i-2行数据,也就是i-2行及以前的数据没有用了,可以清除。同时,第i-1行的数据每个只会用到一次。
(2)遍历每一行时候只用到当前容量j和j-w[i]的数据,也就是第 i 次遍历只需要 第 i-1 次遍历中容量小于等于 j 的数据 。

现在考虑如果我们只用一位数组f[v]来表示f[i][v],是不是同样可以达到效果?

for (int i = 1; i <= n; i++)
{
	for (int j = V; j >= v[i]; j--)
	{
		f[j] = max(f[j], f[j - v[i]] + w[i]);
	}
}

从本质上说,这种优化方法针对了上述的三个问题:
(1)把遍历第i个物体和遍历第i-1个物体时的最大价值存在一个单元里。更新前f[j]存i-1的价值,更新后f[j]存i的价值。因为用不到i-2及以前的数据所以不需要存。因为以后不会再用到i-1的价值所以被覆盖了没问题。
(2)j从背包容量V开始遍历,即从大到小遍历,保证了当前f[j]和f[j - w[i]]里面存的是i-1的数据,即等价于f([i])[j] = max( f [i - 1]  [j], f [i - 1] [ j - v[i] ] + w[i] ),从而和优化空间复杂度前状态转移方程的原理一致。
(3)自己给自己赋值,是无用操作,所以j < v[i]时候什么都不做即可。换句话说,只需要遍历到j >= v[i]。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值