算法 - 01背包

01背包问题


背包问题,在算法题目中是比较常见的系列,题型变化也非常的多,同时每个题目有多种解题思路,本次先来了解一下最经典的01背包的问题。

有N件物品和一个最多能被重量为maxWeight 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

废话不多说,来个具体的题目试试。假设你是一个小偷,背了个最大容量为7kg的包,进入商店偷窃,发现一些可以偷的商品,商品的重量和其对应的价值如下(用下标来一一对应,比如商品0的重量为1kg,价值为1万,商品1重量为2kg,价值为3万)value = [1,3,4,1,2,5,2,4,1,3,2,1,8],weight = [1,2,4,3,2,1,2,4,1,3,2,1,6],计算能偷走的最高价值为多少?

解题思路

掂量一下第一个商品,如果能装下我就装起来再去看剩余商品,如果装不下我就把它忽略掉去看看下一个商品,把所有商品都遍历过,或者背包再也装不下才会停止,然后把所有的可能性对比一下,找出那个价值最多的组合,得到”以第一个商品为中心的情况“最大价值。把所有商品中心的情况都计算一遍,输出一个最大值就是我们想要的结果啦!

问题来了(1)遍历过程中,如何记录包里已经装了哪些商品?(2)遍历过程中,如何知道当前包的重量?(3)遍历过程中,如何知道当前包中商品的总价值?

预想一个函数,入参分别为(1)已装入背包的商品goodsIndex;(2)目前背包里商品的重量currentWeight;(3)目前背包里的商品总价值;最后返回一个”背包里已经装了goodsIndex商品的情况下的“最大总价值maxValue。

那么用什么来模拟背包呢?数组吗?不会吧不会吧,数组中遍历查找元素复杂度可是O(n)啊,不如用Set,Set.has复杂度O(1)而已。

思路如此,便得以下递归算法。

算法初解

const getMaxValue = (weight,,value, maxWeight) => {


    const dpFunc = (goodsIndex, currentWeight, currentValue) => {

        if (goodsIndex.size >= weight.length || currentWeight === maxWeight) return currentValue

        let maxVal = currentValue
        for(let i = 0; i < weight.length; i++) {
            if (goodsIndex.has(i) || weight[i] > (maxWeight - currentWeight)) continue
            maxVal = Math.max(dpFunc(new Set(goodsIndex).add(i), currentWeight + weight[i],currentValue + value[i]), maxVal)
        }

        return maxVal
    }
    return dpFunc(new Set(), 0, 0)
}
const maxValue = getMaxValue(weight, value, maxWeight);


代码优化

这题目可以用肉眼看出,价值最大的下标组合是{5, 12}。当goodIndex={5}的时候,也就是运行dp({5}, 1, 5)时,for循环从i=0开始,绕过i=5,最终会到达i=12那一轮,必定会将goodsIndex置为{5,12},得出总价值为13;

而当goodIndex={12}的时候,也就是运行dp({12}, 6, 8)的时候,for循环从i=0开始,会到达i=5那一轮,必定会将goodsIndex置为{12,5},得出总价值为13;

我们会发现goodsIndex={5,12}和{12,5}都会出现,但他们的价值之和是一样的,那么就可以想象这个算法存在着很多的重复计算,我们干脆给for循环做做手脚,让i从'最后放入背包的商品下标'+1开始遍历。

转念一想,goodsIndex是用来模拟背包的,是为了检验包里有没有第i个商品,既然遍历顺序是从‘最后放入背包的商品下标’+1开始的,那么我为啥还需要goodsIndex这个变量呢?直接传入‘最后放入背包的商品下标’+1 来代替 goodsIndex咯。来试试!

const getMaxValue = (weight,,value, maxWeight) => {
    const dpFunc = (index, currentWeight, currentValue) => {
            if (index >= weight.length - 1 || currentWeight === maxWeight) return currentValue
            let maxVal = currentValue
            for(let i = index; i < weight.length; i++) {
                if (weight[i] > (maxWeight - currentWeight)) continue
                maxVal = Math.max(dpFunc(i + 1, currentWeight + weight[i], currentValue + value[i]), maxVal)
            }
            return maxVal
    }
    return dpFunc(0, 0, 0)
}
const maxValue = getMaxValue(weight, value, maxWeight);

思路转变

遍历到第1个商品的时候,看下当前背包还剩多少容量,如果能容下第1个商品我就装进去然后剩余容量就得减掉第1个商品的重量再继续遍历下一个商品,如果容不下第一个商品那么我就直接查看第二个商品,最终得到‘以第1个商品为开头的’的最大价值。当然我也可以选择直接忽略第一件商品,计算出“以第二个商品为开头”的最大价值,最终把“以每个商品未开头”的最大价值都求出来,输出最大值。如果按照这种思路,我们只需要知道当前遍历到的商品下标index和背包剩余重量,所以我们dpFunc最终的计算结果其实是“以index为开头,剩余重量为lastWeight”的情况下的最大价值。

const getMaxValue = (weight,,value, maxWeight) => {
        const dpFunc = (index, lastWeight) => {
                if (!lastWeight || index === weight.length - 1) return 0
                let maxVal = 0
                for(let i = index; i < weight.length; i++) {
                    if (weight[i] > lastWeight) continue
                    maxVal = Math.max(value[i] + dpFunc(i + 1, lastWeight - weight[i]), maxVal)
                }

                return maxVal
        }
        return dpFunc(0, maxWeight)
}
const maxValue = getMaxValue(weight, value, maxWeight);

使用动态规划

(1)确定二维dp数组,以及下标的含义。

尝试使用一个二维数组,i代表商品编号,j代表背包容量。

dp[i][j]就代表从0到i的商品里取,背包容量为j时能偷到的最大价值。示例图如下:

 (2)确定递推公式。

当求dp[i][j]值的时候,如果weight[i] > j的话就说明背包装不下i商品,那么dp[i][j] = dp[i - 1][j]。如果weight[i] <= j的话,就说明背包还可以装的下i商品,装下i商品dp[i][j] = dp[i - 1][j - weight[i]] + value[i],一定要装下商品i才能使包里商品价值达到最大吗?不一定吧,拿刚计算出的dp[i][j]跟dp[i][j-1]比较一下取较大值并赋给dp[i][j],才能使dp[i][j]符合我们的预期。

(3)确定dp边界。

当j=0时,什么都放不下,商品最大的价值肯定也是0;而当i=0时,dp[0][j] = weight[0] > maxWeight ? value[0] : 0;

(4)按照上述思路完成代码;

const getMaxValue = (weight,,value, maxWeight) => {
        const dp = []
        for(let i = 0; i < value.length; i++) {
                dp.push([0])
                for(let j = 1; j <= maxWeight; j++) {
                    if (i) {
                            if (j >= weight[i]) {
                                    maxVal= Math.max(dp[i - 1][j - weight[i]] + value[i], dp[i - 1][j])
                            } else {
                                    maxVal = dp[i - 1][j]
                            }
                    } else {
                            maxVal = j >= weight[i] ? value[i] : 0
                    }

                    dp[i].push(maxVal)
                }
        }
        return dp[value.length - 1][maxWeight]
}


const maxValue = getMaxValue(weight, value, maxWeight);

(5)数组降维

通过分析状态转移方程,j >= weight[i] ? dp[i][j] = Math.max(dp[i - 1][j - weight[i]] + value[i], dp[i - 1][j]) : dp[i][j] = dp[i - 1][j];

我门发现取值的时候会有dp[i - 1][x]这一项,且一旦遍历到i = n时,dp[n - 2]这个数组就不再需要了,所以我们只需要将dp[n - 1]这个数组给保存下来就可以摆脱二维数组了。

哎?那我们能不能只使用一个数组DP,前i个商品的最大价值用DP[j]来表示,那么当遍历到(i,j)时,原来的dp[i - 1][j]的值就是计算前的DP[j],然后我们拿原来计算出来的dp[i][j]来更新DP[j]。原来的dp[i - 1][j - weight[i]]就是DP[j - weight[i]]吗?不见得, 因为遍历到(i, j)的时候必然经过了(i, j - weight[i]),所以新数组DP[j - weight[i]]已经被更新过了,即为原来的dp[i][j - weight[i]],那么怎么办呢?其实,我们倒着遍历j应该可以解决这个问题了,仔细想想应该也不会有所谓的边界问题。按照这个思路来改进一下算法吧~

const getMaxValue = (weight,value, maxWeight) => {
        const DP = new Array(maxWeight + 1).fill(0);

        for(let i = 0; i < weight.length; i++) {
                for(j = maxWeight; j > 0; j--) {
                    if (weight[i] <= j) DP[j] = Math.max(DP[j - weight[i]] + value[i], DP[j])
                }
        }
       return DP[maxWeight]
}
const maxValue = getMaxValue(weight, value, maxWeight);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值