算法-01背包-一维DP数组

loading 2022年11月25日 73次浏览

1. 引入

之前做01背包都是用的二维dp数组,如果是二维dp数组,那么用到的是上方和左上方的数据。

可以发现当我们要计算i+1这行数据时,i-1这行的数据就用不到了。

也就是说,第i行第j个状态只和第i-1行第j个状态、第i-1行第j-weight[i]个状态有关。

那么我们就可以把i这个维度压缩掉,只留下j这个维度:

dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);

//对比上面的式子 其实就是把[i-1]这个维度去掉了
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);

再分析一次:

等式左边的dp[j]压缩的是dp[i][j],代表第i层的dp;

等式右边的dp[j]和dp[j - weight[i]]代表第i-1层的dp

而我们使用for循环时枚举物品,也就是'i'这一维度的时候是正序的,即从1到2,从2到3...从i-1到i。

也就是:用i-1状态的一维dp数组作为i状态的一维dp数组的初值(因为i状态只和i-1状态有关),因此被称为滚动数组。

2. 分析

2.1 确定dp数组的意义

dp[j]表示,容量为j的背包,所能承载的编号为0至i-1物品的最大价值。(注意dp[j]只考虑前i-1个物品,不放第i件时)

2.2 状态转移方程

当我们来到dp[j]时,dp[j]里的值就是上一层dp的值。

两个选择:

  • 不放物品i
    不放物品那容量也就不变,价值自然也不变了,还是dp[j]

  • 放物品i
    放物品那就是容量减少,价值增加:dp[j - weight[i]] + value[i]

因此也就可以得出如上的状态转移方程,相当于去掉了dp[i][j]中i的维度

dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);

2.3 初始化

首先可以确定dp[0] = 0,因为容量为0的背包装不了东西,价值肯定是0。

那么其他位置呢,首先不能像二维数组那样初始完两条边后剩下的随便初始化都行,因为二维数组中间的位置的值都是由左上方和上方的值得来的,自己的初始值肯定会被覆盖掉。

而一维数组中dp[j]是通过dp[j]和dp[j-weight[i]]推导过来的,而且接下来还要取max值,因此所有数全部初始化为0,否则会出现max值被初始值覆盖的问题。

2.4 遍历

先上代码

        function bag01Problem(weight, value, load) {
            const len = weight.length;
            //遍历物品
            for (let i = 0; i < len; i++) {
                //遍历背包容量
                for (let j = load; j >= weight[i]; j--) {
                    dp[j] = Math.max(dp[j], dp[j] - weight[i] + value[i]);
                }
            }
        }

两个注意点:

(1)背包容量要倒序遍历

倒序遍历是为了保证物品i只被放入一次,如果选择正序遍历,那么物品0就会被重复加入多次。

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[5]

dp[5] = dp[5 - weight[0]] + value[0] = dp[4] + value[0] = 15 (dp数组已经都初始化为0)

dp[4] = dp[3] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

二维数组不用倒叙遍历,是因为自身数据都是由上方和左上方计算得来,本层的dp[i][j]不会被覆盖。

(2)for循环顺序不能变

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

因为滚动数组的特性,我们必须先将i-1这层的j全部遍历完,然后再进到第i层把dp[i-1][j]的数据赋值给dp[i][j],然后第i层才开始根据赋值过来的数据和状态转移公式进行计算。

最后附上完整代码:

        function testWeightBagProblem(weight, value, load) {
            const len = wight.length;
            const dp = new Array(load + 1).fill(0);
            for (let i = 0; i < len; i++) {
                for (let j = load; j >= weight[i]; j--) {
                    dp[j] = Math.max(dp[j] , dp[j - wight[i]] + value[i]);
                }
            }
            return dp[load];
        }

        function test() {
            console.log(testWeightBagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6));
        }

        test();