0-1背包问题(动态规划的做法,不是回溯)

0-1背包问题有很多解法,这里我们用动态规划做,暴力破解我后续会写出来,因为涉及到回溯,所以我会在回溯中介绍这个。

0-1背包是干啥的大家肯定都不陌生了,最简单最朴素的就是当前有n个物品每个物品有自己价值且物品不可拆分,有一个容量m的背包,问这个背包如何装物品可以使得价值最大

那我们就用一个具体的例子来解释这个问题:

首先我们有一个物品清单和一个容量为4背包,如下图:

直观上,大家肯定可以得出答案,就是单方物品2是价值最大的。但代码如何实现呢?

这里先做一些准备工作,我们需要dp二维数组(下图绿色框框),以及一个weight数组,value数组分别存放物品的重量和价值,还有一个bag_weight存放包的容量,那这里为了直观解释,我们做了一些改动,具体如下图:

数组准备好后,我们开始往里面填充数值,这个表格就是这个算法的核心,我概括了一下,大致分为3步,也就是三步走:

  • 第一步:初始化,也就是把第0行和第0列分别进行初始化,代码如下:
    for(int i=bag_weight;i>=weight[0];i--)
    {
        dp[0][i]=dp[0][i-weight[0]]+value[0];
    }

对上面的代码我做如下解释,见下图:

至此初始化工作完成,这里提一嘴,可不可以不初始化,直接把整个数组赋值为0,这不好嘛?

答:不可以,这个例子是没问题,但其他题大概率是不行的,原因可以自己想一下。

这里再解释一下表格中数据的含义,方便后面大家对代码的理解。

  • 第二步:遍历(更新)二维数组剩余部份,代码如下:
 for(int i=1;i<weight.size();i++)
    {
        for(int j=1;j<=bag_weight;j++)
        {
            if(j < weight[i]) 
                dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);     //精髓所在
        }
    }

上述代码使用双循环来遍历更新数组内容,其中外循环是遍历物品,内循环是遍历背包的容量,具体解释见下图:

  • 第三步:输出最后的结果,也就是最右下角的数

下面是完整代码,大家可以运行一下,感受一下代码具体的细节:

#include<iostream>
#include<vector>
using namespace std;

vector<int> weight={1,3,4};
vector<int> value={15,20,40};
int bag_weight=4;

//这一步就是给“二维数组”赋值为全0
vector<vector<int>> dp(weight.size(),vector<int>(bag_weight+1,0));

void beibao()
{
    //初始化,不要全给0,那是憨憨,解释一下j-weight[0]就是当前包的容量——这个0号物体的重量还剩多少,也可能不够装
    for(int i=bag_weight;i>=weight[0];i--)
    {
        dp[0][i]=dp[0][i-weight[0]]+value[0];
    }

    //更新dp数组,这里就是dp的精髓了,从dp[1][1]开始更新啊,别傻乎乎的dp[0][0]开始
    for(int i=1;i<weight.size();i++)
    {
        for(int j=1;j<=bag_weight;j++)
        {
            if(j < weight[i]) 
                dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);     //精髓所在
        }
    }

    //最后输出二维数组右下角的数,这个数就是背包可以装的最大容量
    cout<<dp[weight.size()-1][bag_weight];
}

int main()
{
    beibao();
    return 0;
}

0-1背包问题——一维数组(滚动数组)

上面我们介绍了0-1背包的动态规划的解法,用一个dp二维数组来存放背包的状态,但其实大家如果掌握的熟练其实很快就可以发现其实dp二维数组中是有状态的重叠的,啥意思呢?见下图:

上图解释了为啥可以用一维数组存储这个结果,说白话就是一维数组可以重复利用,直接把二维数组中的上一层拷贝到下一层。

那下面我们来实际操作一下,还是那之前的例子,同样的一个weight数组,一个value数组,一个bag_weight变量:

接下来就是dp的五步走:

  • 1、确定dp数组的含义

之前我们用二维数组的时候,dp[i][j]的含义还记得嘛?不记得赶紧往上翻去复习。这里用一维数组,那么就是dp[j]。仔细看,我写的是dp[j],而不是dp[i]哦。这么写就是为了匹配二维数组,这里写dp[j]就是背包容量为j时最大价值,也就是说j依旧表示背包容量。

  • 2、dp数组的递推公式,大家自行理解,和二维数组的递归公式差不多的。
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
  • 3、dp一维数组的初始化

这里解释一下为啥这么初始化,我们的核心就是保证数组中的元素都是背包当前容量的最大价值,你别初始化的值把这个最大价值覆盖了,那不行的。
  • 4、确定遍历(更新)dp数组的顺序,谁在前谁在后,循环从前往后还是从后往前
for(int i=0;i<weight.size();i++)
{
    for(int j=bag_weight;j>=weight[i];--j)
    {
        dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
    }
}

我解释一下上述代码的一些细节问题:

  • 5、遍历(更新)数组,不熟练就在纸上把数组得元素填满感受一下

下面给出完整代码,供大家体会理解,也可以单步调试:

#include<iostream>
#include<vector>
using namespace std;

vector<int> weight={1,3,4};
vector<int> value={15,20,40};
int bag_weight=4;

vector<int> dp(bag_weight,0);

void func()
{
    for(int i=0;i<weight.size();i++)
    {
        for(int j=bag_weight;j>=weight[i];--j)
        {
            dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
        }
    }
    cout<<dp[bag_weight]<<endl;
}

int main()
{
    func();
    return 0;
}

lionの金库