0-1 背包问题

0-1 背包问题

一、典型题目

n种物品,每种只有1个。( 因此称为0-1背包问题——对应着每个物体 选与不选 的两种选择)

第 i 种物品的体积为 V i V_i Vi$ ,重量为 $ W i W_i Wi

背包容量为 V 。

选物品装到背包,使得背包内的物品在总体积不超过C的前提下重量尽量大。

二、暴力破解的尝试

​ 如果采用暴力破解,用回溯法,那么时间复杂度将会为 O( 2 n 2^n 2n) ,这显然是行不通的,于是我们开始用动态规划的思想来处理。

三、二维数组的尝试

1、动态规划的核心是状态和状态转移方程

1.1、状态

​ f [ i ] [ j ] ——定义:前 i个物品,背包容量 j下的最优解

​ 我们据此可以发现一个关系——当前的状态是依赖于之前的状态的;同时,当前的状态是在之前的状态的基础上来选择 对于当前的物品要不要选的 两种决定。

​ 换言之,对于f [i] [j],可以由两个方面来做决定,(假设容量够)在f[i-1] [ j ]的基础上,①不要当前的第i个,②要当前的第i个。这就引出了状态转移方程。

1.2、状态转移方程

(1)容量不够:那么 f[ i ] [ j ]=f[i-1] [j]

(2)容量够:

​ ①不要当前的。那么 f[ i ] [ j ]=f[i-1] [j];

​ ②要当前的。那么 f[i] [j] = f[i - 1] [ j - v[i] ] + w[i]

(对于②为什么? 当我们考虑当前的状态是,我们就需要考虑“刚才那个阶段的状态”,也就是说:我们想要在当前放入v[i] (注意 此刻的 最大容量记为了 j ),就需要要求前一个的 最大值 得是 j - v[i] ,于是我们得出来了前一个的基础为f[i - 1] [ j - v[i] ],此时的价值在原基础上加上i的价值(w[i])即可)

​ 综上,我们就可以来决定当前的最优解怎么选了——选或者不选当前的第i个比较后取最大值,f[i] [j]=max (f[i-1] [j] , f[i-1] [j-w[i]]+v[i] )

2、代码

... 
    
	for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
        {
            //  容量不够用
            if(j < v[i])	f[i][j] = f[i - 1][j];
            // 容量够用
            else	f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }      

    cout << f[n][m] << endl;

...

四、二维下的一些考虑的问题

1、f [MAX] [MAX] 初始化——全为“0”(零) 

​ f[0] [0]=0 ,背包容量为0,什么也装不下,同理f[i] [0]=0;

​ 与之对应的,所选物品数量为零,什么也不装,f[0] [i]=0;

​ 由此,全部赋值为0。

2、二维数组的利用率

假设(后续表格展示的前提假设亦是如此):

背包容量max=4;

物品1–重量1 价值 15;

物品2–重量3 价值 20;

物品3–重量4 价值 30;

则:

物品 i \ 容量 j01234
000000
1015151515
2015152035
3015152035

我们发现:每个“阶段”所用的只是当前的和“上一轮的”,于是就会出现可以 压缩空间 的问题,就有了优化方案——滚动数组

五、基于二维数组的优化——一维数组的尝试

其实,我们只需要简单的做个等价变形即可:

for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)	//改为 逆序 枚举
    {
        if(j < v[i]) 
    //      f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后
        else    
    //      f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
    }    

于是:

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

f [j]表示:容量为j的背包,所背的物品价值可以最大为f [j]。

六、一维数组尝试中的思考

1、为什么 j 采用逆序

​ ①比较优化前后:如果不逆序,优化后的f[j - v[i]]指的是当前的f[i] [j - v[i]],而非我们所期望的f[i - 1] [j - v[i]];

​ ②顺序枚举可能会导致重复装入:我们通过二维数组的尝试可以知道,第i个是依赖于第i-1个的——于是:如果采用正序,当i=1计算完后,数组内会存有值,当i=2时,会在此基础上再从j=0进行判断,此时就有可能会再加入一次之前已经加过的值(而0-1背包只能加一次);但是采用逆序的话,则不会存在重合的状态——综合来看问题就在于当遍历 j 到一半(或者一部分)时,此时数组内存在着两个“阶段”的数据,而使用数据都是后面位置用前面位置的,要想互不影响,只好从后面开始遍历,这样是用的前面位置的数据就会是“上一个阶段”的。(同时,这也解释了为什么二维数组的尝试不需要逆序遍历——每“层”的(每个“阶段”的,对应上述文本中的表格一行)数据都是同一个“阶段”的,不会被覆盖)

2、为什么一位数组的尝试只能先遍历物品再遍历容量

关键在于逆序遍历的写法。如果不是 先遍历物品再遍历容量,而是反过来的话,就会让每次遍历f[j](容量为j的背包,所背的物品价值可以最大为f [j])只能放一个物品(背包只放了一个物品)。

  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值