问题描述

一共有 N 件物品,第 i (i 从 0 开始)件物品的重量为 w[i],价值为 v[i] 。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?

暴力递归

分析题目可知,对于某一件商品 i ,你只能做出两个选择:要,或者不要。那么我们用递归穷举所有情况,每一件商品都尝试要或者不要,最后选出最大的价值。容易得到下面代码:

//index-商品序号
//take-要或者不要
//capacity-剩余容量
int process1(const vector<int> &weight, const vector<int> &value, int index, bool take, int capacity)
{
    if(index >= weight.size() || capacity < 0) //序号大于商品数量或背包容量不足时,返回0
        return 0;
    if(take)//如果要此商品
    {
        if(weight[index] > capacity)//那么判断是否装得下该商品
            return 0;
        //capacity - weight[index],将该商品放入背包,容量减少
        int ret1 = process1(weight, value, index + 1, true , capacity - weight[index]);//要下一件商品
        int ret2 = process1(weight, value, index + 1, false, capacity - weight[index]);//不要下一件商品
        return value[index] + std::max(ret1, ret2);//返回本商品价值+后续的较大值
    }
    else//如果不要此商品
    {
        //无需消耗capacity
        int ret1 = process1(weight, value, index + 1, true , capacity);//要下一件商品
        int ret2 = process1(weight, value, index + 1, false, capacity);//不要下一件商品
        return std::max(ret1, ret2);//返回后续的较大值
    }
}

int knapSack1(vector<int> weight, vector<int> value, int capacity)
{
    if(weight.size() != value.size())
        return 0;
    //从序号0的货物开始尝试要或者不要
    return std::max(process1(weight, value, 0, true, capacity), process1(weight, value, 0, false, capacity));
}

注意基线条件 capacity < 0 ,也就是说 capacity 可以等于 0,因为可能出现重量为 0 但价值不为 0 的货物。

动态规划

int knapSack2(vector<int> weight, vector<int> value, int capacity)
{
    if(weight.size() != value.size())
        return 0;
    vector<vector<vector<int>>> dp;//dp[index][capacity][take]
    //先统一初始化表
    dp.resize(weight.size());
    for(int i = 0; i < weight.size(); i++)
    {
        dp[i].resize(capacity + 1);//注意+1!!!!
        for(int k = 0; k <= capacity; k++)
        {
            dp[i][k].resize(2);
            dp[i][k][0] = 0;
            dp[i][k][1] = 0;
        }
    }
    
    //特例初始化
    int s = weight.size();
    for(int i = 0; i <= capacity; i++)
    {
        if(weight[s-1] <= i)
            dp[s-1][i][1] = value[s-1];
        dp[s-1][i][0] = 0;
    }

    //依次打表
    for(int i = s - 2; i >= 0; i--)
    {
        for(int k = 0; k <= capacity; k++)
        {
            dp[i][k][0] = std::max(dp[i+1][k][1], dp[i+1][k][0]);
            if(weight[i] <= k)
                dp[i][k][1] = value[i] + std::max(dp[i+1][k-weight[i]][1], dp[i+1][k-weight[i]][0]);
        }
    }
    return std::max(dp[0][capacity][1], dp[0][capacity][0]);
}

第 19~26 的特例初始化是什么意思?由暴力递归中的递推表达式可知,当前 index 依赖于 index + 1,所以填表必须按 index 从后往前进行,因此我们需要先求出最后的 index 对应的表格值。最后的 index 即为 weight.size-1 ,如果我们要这个这个商品,则先判定剩余容量是否能够装下它,如果能,则直接将对应表格赋值为 value[s-1] ;如果不要则赋值为 0 。 这些操作其实都和递归函数里的操作相对应。

capacity 则是后面依赖前面的,所以打表必须按 capacity 从前往后打,故内层 for 循环是从 k = 0 开始的


优化参数

上面的方法中我们使用了三个状态变量,导致填表操作有些繁琐。实际上,take 参数完全可以省略,它的存在只是为了帮助我们理解思路,反而复杂化了函数的递归流程。以下是优化 take 参数后的版本:

int process3(const vector<int> &weight, const vector<int> &value, int index, int capacity)
{
    if(index >= weight.size() || capacity < 0)//capacity可以为0
        return 0;
    int ret1 = process3(weight, value, index + 1, capacity);//不要此货物
    int ret2 = 0;
    if(weight[index] <= capacity)//当前容量够,才能要此货物
        ret2 = value[index] + process3(weight, value, index + 1, capacity - weight[index]);//要此货物
    return std::max(ret1 , ret2);
}
int knapSack3(vector<int> weight, vector<int> value, int capacity)
{
    if(weight.size() != value.size())
        return 0;
    return process3(weight, value, 0, capacity);
}

process3process2 中的两个 if 分支进行了合并,有完全一样的效果,并大大简化了代码和流程。

动态规划-改进

根据 process3 很容易得出下面代码:

int knapSack4(vector<int> weight, vector<int> value, int capacity)
{
    vector<vector<int>> dp(weight.size(), vector<int>(capacity + 1, 0));
    int last = weight.size() - 1;
    
    //特例初始化
    for(int c = 0; c <= capacity; c++)
    {
        if(weight[last] <= c)
            dp[last][c] = value[last];
    }
    
    //依次打表
    for(int i = weight.size() - 2; i >= 0; i--)
    {
        for(int c = 0; c <= capacity; c++)
        {
            int ret1 = dp[i+1][c];
            int ret2 = 0;
            if(weight[i] <= c)
                ret2 = value[i] + dp[i+1][c-weight[i]];
            dp[i][c] = std::max(ret1, ret2);
        }
    }
    return dp[0][capacity];
}

另外,也可以不使用特例初始化,直接统一处理,但前提是每次填表时都必须检查基线条件,这在动态规划入门-机器人行走中已经说明。统一处理的代码如下:

//不做特例初始化,统一处理  
int help(int index, int capacity, int size, const vector<vector<int>> &dp)  
{  
    if(index >= size || capacity < 0)  
        return 0;  
    return dp[index][capacity];  
}  
int knapSack6(vector<int> weight, vector<int> value, int capacity)  
{  
    vector<vector<int>> dp(weight.size(), vector<int>(capacity + 1, 0));  
    for(int i = weight.size() - 1; i >= 0; i--)  
    {  
        for(int c = 0; c <= capacity; c++)  
        {  
            int ret1 = help(i+1, c, weight.size(), dp);  
            int ret2 = 0;  
            if(weight[i] <= c)  
                ret2 = value[i] + help(i+1, c-weight[i], weight.size(), dp);  
            dp[i][c] = std::max(ret1, ret2);  
        }  
    }  
    return dp[0][capacity];  
}

总结

  • 这类从左往右且可以归为“要或不要”的题型,都能使用类似方法解决。
  • 尽可能优化参数,减少参数数量。
文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
数据结构与算法
喜欢就支持一下吧