动态规划之0-1背包问题以及其空间优化讲解

开始背包问题讲解前,我们先粗略的了解一下动态规划。动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。

一、0-1背包问题

给定n个重量为w1, w2, w3, … , wn, 价值为v1, v2, v3, … , vn 的物品和容量为C的背包,求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大。
0-1背包问题指的是每个物品只能使用一次。

二、求解思路

动态规划
第一步骤:定义数组元素的含义。开头说了,我们会用一个数组来储存历史数据,假设为dp[],然后我们要确定dp[i]代表什么意思。一般题目需要求解什么,便将其设为dp[]。
第二步骤:找出要素之间的关系,即确定转移方程。找dp[i], dp[i-1]…之间的关系,或者对于二维数组,找dp[i][j], dp[i-1][j], dp[i-1][j-1], dp[i][j-1]等等之间的关系。
第三步骤:找出初始值,即不能再分解的dp[i]。

背包问题求解思路
第一步骤:设dp[i][j],i代表N种物体中的第i个,j代表此时背包最多负重为j,dp[i][j]代表在只能装前i种物体和包的容量为j的前提下,包内最大的总价值。
第二步骤:确定状态转移方程
0-1背包中一个状态就是N个物体中第i个是否放入体积为j背包中,由此确定转移方程。

if (背包体积j小于物品i的体积)
    //背包装不下第i个物体,目前只能靠前i-1个物体装包,因为j<val[i], 所以即使包中只装物品i也装不下。
    f[i][j] = f[i-1][j] 
else
    //f[i-1][j]不装物品i,f[i-1][j-Vi]装物品i,j-Vi意思是包中已经装了j-Vi,在装Vi,而且显然j-Vi>=0~(j-Vi-1)所包含的价值(递增)
    f[i][j] = max(f[i-1][j], f[i-1][j-Vi] + Wi)

第三步骤:确定初始状态。初始状态为f[0][0−V]和f[0−N][0]都为0,前者表示前0个物品(也就是空物品)无论装入多大的包中总价值都为0,后者表示体积为0的背包啥价值的物品都装不进去。

三、程序

#include<iostream>
using namespace std;

int main()
{
    int nArr[6][13] = {{0}}; //全部初始化为0
    int nCost[6] = {0 , 2 , 5 , 3 , 10 , 4};  //花费
    int nVol[6]   = {0 , 1 , 3 , 2 , 6 , 2}; //物体体积
    int bagV = 12;
    //在只装前i种物品前提下,求重量为j的包所含有的最大价值。
    for( int i = 1; i< sizeof(nCost)/sizeof(int); i++)
    {
        for( int j = 1; j<=bagV; j++)
        {
            if(j<nVol[i])
                nArr[i][j] = nArr[i-1][j];
            else
                nArr[i][j] = max(nArr[i-1][j] , nArr[i-1][j-nVol[i]] + nCost[i]);       
            cout<<nArr[i][j]<<' ';
        }   
        cout<<endl;
    }
    cout<<nArr[5][12]<<endl;

    return 0;
}

01背包问题其实就可以化简为涂写下面的表格,其中每个数对应数组nArr中每个元素,初始化部分为0,然后从左上角按行求解,一直求解到右下角获取最终解nArr[5][12]。
在这里插入图片描述
图来自最通俗易懂的01背包问题讲解

四、空间优化

从上述代码可知我们的空间复杂度为O(mn),我们可以看到状态转移方程右边部分,dp[i][j]貌似只与上一行(i-1)有关,而其余行我们根本没有必要去记录,因此我们只需要一个一维数组就行了,这样空间复杂度也就降到了O(n)。
若不懂,在这篇知乎上有个很详细讲解告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我(万字长文)
我们先上优化后的代码:

#include<iostream>
using namespace std;

int main()
{
    int nArr[6][13] = {{0}};
    int nCost[6] = {0 , 2 , 5 , 3 , 10 , 4};  //花费
    int nVol[6]   = {0 , 1 , 3 , 2 , 6 , 2}; //物体体积
    int bagV = 12;

    for( int i = 1; i< sizeof(nCost)/sizeof(int); i++)
    {
        for( int j = bagV; j>=nVol[i]; j--)
        {
            nArr[j] = max(nArr[j] , nArr[j-nVol[i]] + nCost[i]);       
            cout<<nArr[i][j]<<' ';
        }   
        cout<<endl;
    }
    cout<<nArr[5][12]<<endl;

    return 0;
}

看完代码我们会很疑惑,为什么第二层for循环是倒序?
先引用网上的一小段话:


f[i][v]只与f[i-1][v]和f[i-1][v-C[i]]有关,即只和i-1时刻状态有关,所以我们只需要用一维数组f[]来保存i-1时的状态f[]。
假设i-1时刻的f[]为{a0,a1,a2,…,av},难么i时刻的f[]中第v个应该为max(av,av-C[i]+W[i])即max(f[v],f[v-C[i]]+W[i]),
这就需要我们遍历V时逆序遍历,这样才能保证求i时刻f[v]时f[v-C[i]]是i-1时刻的值。如果正序遍历则当求f[v]时
其前面的f[0],f[1],…,f[v-1]都已经改变过,里面存的都不是i-1时刻的值,这样求f[v]时利用f[v-C[i]]必定是错的值。最后f[V]即为最大价值


我们引用01背包问题 总结关于为什么01背包优化成1维数组后,内层循环是逆序的?的一个例子:
设有3件物品 ,背包能容纳的总重量为10
i=1,2,3

物品号 重量© 价值(w)
i=1 4 5

i=2 7 9

i=3 5 6

f[v]=max{ f[v],f[v-c[i]]+w[i] }

如果v是顺序递增 i=1时,v=4~10 (因为v要至少大于等于c[i]嘛 不然减出个负数没意义)
原先的: f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=0 f[5]=0 f[6]=0 f[7]=0 f[8]=0 f[9]=0 f[10]=0
--------------------------- i=1 --------------------------------
后来的: f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=5 f[5]=5 f[6]=5 f[7]=5 f[8]=10 f[9]=10 f[10]=10
v=4:
f[4]=max{f[4],f[0]+5} max{0,5}=5 f[4]=5

v=5:
f[5]=max{f[5],f[1]+5} max{0,5}=5 f[5]=5

v=6:
f[6]=max{f[6],f[2]+5} max{0,5}=5 f[6]=5

v=7:
f[7]=max{f[7],f[3]+5} max{0,5}=5 f[7]=5

v=8:
f[8]=max{f[8],f[4]+5} max{0,10}=10 f[8]=10 (这里显然不对,这时i=1,只能放一件物品,然而没有一个物品的价值为10的 )

v=9:
f[9]=max{f[9],f[5]+5} max(0,10}=10 f[9]=10

v=10:
f[10]=max{f[10],f[6]+5} max{0,10}=10 f[10]=10
从上面例子中我们可能就会明白其中道理了,如果我们正序遍历,在i=1时候如果背包的容量>=2倍的第一个物体的重量,那么又会被装一遍,这与0-1背包只能装一次的原则相悖,究其原因是f[i-1]不是i-1时刻的值了,而是i时刻的值,因此需要倒序,这样才不能改变i-1时刻的值。而未优化的O(mn)方法,则不需要理会这个,因为i-1时刻的值已经被保存在二维数组里了。

*五、扩展

其实顺序可以解决另一种问题,也是背包问题,但不是0-1背包问题,也就是每种物品可以被取无限次。
洛谷上的一道题:P1616 疯狂的采药

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值