背包问题(Knapsack problem)

背包问题初解:

分析:

定义:
设 dp[i][j] 为在背包容量为 j 时任取下标为 [0 ~ i] 的物品所得的最大价值。

递推关系:
1.一行一行填,在第 i 行时只考虑放不放入下标为 i 的物品:
2.如果不放下标为 i 的物品,剩余下标为 [0 ~ i-1] 的物品可选,背包容量剩余 j,则最大价值为 dp[i-1][j]; 
3.如果可以放入且放入下标为 i 的物品,剩余下标为 [0 ~ i-1] 的物品可选,背包容量剩余 j-weight[i],则最大价值为 value[i] + dp[i-1][j-weight[i]]; 

*约束条件:
如果背包容量比当前要选择放的物品(下标为i)的重量小,那么就不放入物品 i ,此时最大价值为 dp[i-1][j];
如果背包容量比当前要选择放的物品的重量大,那么可以选择放入或者不放入,为了使价值最大,需要比较两种情况,此时最大价值为:
max(dp[i-1][j],dp[i-1][j-weight[i]])

*初始化:
1. dp[i][0] 表示背包容量为 0 时 任意放入下标为 [0 ~ i-1] 的物品的最大价值,易知
dp[i][0] = 0。
3. 初始化第 0 行时,当 j >= weight[0] 时的最大价值都为 value[0],即 dp[0][j] =
value[0]。

最大价值:返回dp[最后一个物品的下标][背包容量]。

例如:

vector<int> weight = {1, 3, 4};    // 存储物品所需容量
vector<int> value = {15, 20, 36};  // 存储为物品价值
int bagsize = 4;

初始化:

int n = weight.size(); // 物品个数
// 初始化
vector<vector<int>> dp(n, vector<int>(bagsize + 1, 0));
for (int j = weight[0]; j <= bagsize; ++j) {
    dp[0][j] = value[0];
}

完整代码:

vector<int> weight = {1, 3, 4}; // 物品重量
vector<int> value = {15, 20, 30}; // 物品价值
int bagsize = 4; // 背包容量

int getMaxValue(vector<int>& weight, vector<int>& value, int bagsize) {
    int n = weight.size(); // 物品个数
    // 初始化
    vector<vector<int>> dp(n, vector<int>(bagsize + 1, 0));
    for (int j = weight[0]; j <= bagsize; ++j) {
        dp[0][j] = value[0];
    }
    // 递推
    for (int i = 1; i < n; ++i) {
        for (int j = 0; j <= bagsize; ++j) {
            if (j < weight[i]) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = max(dp[i - 1][j], value[i] + dp[i - 1][j - weight[i]]);
            }
        }
    }
    return dp[n - 1][bagsize];
}

注:时间复杂度近似为O(n*bagsize),空间复杂度近似为为O(n*bagsize)

优化:

对于0/1背包问题,动态规划表格 dp 的空间复杂度可以进一步优化到 O(bagsize)。因为我们可以只保留当前行和上一行的数据(下层继承上层),从而避免使用整个二维数组。这种方法通常称为“滚动数组”优化。

int getMaxValue2(vector<int>& weight, vector<int>& value, int bagsize) {
    int n = weight.size();  // 物品数量
    vector<int> dp(bagsize + 1, 0);
    for (int i = 0; i < n; ++i) {  // 外循环遍历物品
        // 内循环倒序遍历
        for (int j = bagsize; j >= weight[i]; --j) {
            dp[j] = max(dp[j], value[i] + dp[j - weight[i]]);
        }
    }
    return dp[bagsize];
}

思考与解答: 

1.为什么内循环采用了逆序遍历?

如果内循环采用正序遍历(与二维dp一样),则当我们先用物品 0 遍历时,会依次填入0 15 30 45 60。这是错误的。由于没有使用二维数组,使得我们每次填最大价值时都基于了前面一列的最大价值:

例如,在判断dp[2]时,dp[1]已经选择了物品 0,但背包还剩余 1 容量,使得在与dp[1]比较时,又一次装入了物品 0,以此类推。

2.为什么二维dp不会出现重复取物品呢?

因为对于二维dp,dp[i][j]是通过上一层dp[i-1][j]计算而来的,使得本层的dp[i][j]不会被覆盖。

3.为什么逆序遍历时不会重复取物品呢?

以所给例子具体为例:

我们初始化了一维数组为全 0 数组,因此在用物品 0 逆序遍历时,我们始终将 dp[value[i]+j-weight[i]] 与 0 比较,保证每次最多装入一个物品。而在我们用物品 1 遍历时,我们判断出此时能装入物品 1,因此我们比较了 value[1]+dp[3](=35) 与 dp[4](=15)。而这个dp[3]即是由上层“继承”下来的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值