01背包问题

发布时间 2023-08-30 17:47:29作者: tryingWorm

01背包问题

一.问题描述

背包问题的基本条件

现有(n + 1)种物品,每种物品只有一个,编号由0到n,每种物品有两个属性,质量weight,价值value;有一个背包,容量(最大承受质量)为capacity;

为了描述每一种物品,我们使用w[n + 1]和v[n + 1]来描述,因此描述第i种物品时,我们使用w[i]表示其质量,v[i]表示其价值

现在往背包中装物品,要求所装入物品的质量和不超过capacity,求装入物品的最大价值

二.解决方案

背包问题是动态规划的经典题型,因为最终的问题的状态可以由前面的状态一步一步推导而来,而且推导过程中存在重复。分析动态规划问题我使用Carl哥的动态规划五部曲。

1.明确dp数组含义

dp[i][j]表示假设有i + 1种物品(编号0 - i),背包容量为j的情况下,最大的装入物品总价值为dp[i][j]

2.确定递推公式

确定递推公式时,我的思考方式是思考dp[i][j]如何使用前面的状态(dp[x][y])推导而来。我们从dp实际含义的角度出发,发现dp[i][j]只有可能在两种情况下取:

  1. 装的物品中没有第i号物品

    这种情况下,问题转换为在物品最多考虑到第i - 1号,背包容量为j时,其获取的最大价值。由前面dp数组的含义知,最大价值为dp[i - 1][j]

  2. 装的物品中有第i号物品

    这种情况下,先算物品最多考虑到第i - 1号(因为每种物品只有一个),背包容量为j - w[i]时(要留出放第i号物品的空间),获取的最大价值为dp[i - 1][j - w[i]],然后再加上第i号物品的价值v[i],所以最大价值为dp[i - 1][j - w[i]] + v[i]

然后我们只要取这两种情况的最小值就可以了,即min(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])

当然,我们看公式就可以发现这个公式里j - w[i] >= 0,我们结合实际意义发现,j < w[i]意味着背包只放第i号物品都放不下,所以一定是装的物品中没有第i号物品这一情况。

3.确定递推初始化条件

根据公式dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]),我们可以清楚地看到,i - 1 >= 0,即i >= 1,这样就意味着我们只要确定了i = 0的一行,其他结果就能推出来。根据dp的实际意义,dp[0][j]代表着在只考虑第0号物品,背包容量为capacity的时候可以获取的最大价值。显然,只要当j >= w[0],背包就可以放下0号物品,dp[0][j]就可以取得最大价值v[0]。

4.确定递推顺序

根据公式dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]),可以看到,dp[i][j]是由左上的数据推出来的,因此,只要保证dp[i][j]所依赖的左上的数据在算dp[i][j]时已经推出来了就行,所以只要保证从左到右,从上到下的顺序就行了,先遍历i还是j是无所谓的。

先遍历i再遍历j的代码如下

for(int i = 1; i < nums.lengtd; i++){
    for(int j = 0; j <= capicity; j++){
        //递推代码
    }
}

递推顺序如下所示

1(dp[i - 1][j - w[i]]) 2 3(dp[i - 1][j]) 4 5
6 7 8(dp[i][j]) ...

表格中的顺序就是dp[i][j]被计算出的顺序,i代表行,j代表列

先遍历j再遍历i的代码如下

for(int j = 0; j <= capicity; j++){ 
    for(int i = 1; i < nums.lengtd; i++){
        //递推代码
    }
}

递推顺序如下所示

1(dp[i - 1][j - w[i]]) 5 9(dp[i - 1][j])
2 6 10(dp[i][j])
3 7 ...
4 8

表格中的顺序就是dp[i][j]被计算出的顺序,i代表行,j代表列

5.举例推导dp数组

这是为了验证公式的正确性,这里省略了

6.实现代码

根据上面的分析,很容易写出以下实现代码

	public static int bag01(int[] weight, int[] value, int capability){
        int[][] dp = new int[weight.length][capability + 1];
        //初始化
        for(int j = 0; j <= capability; j++){
            if(weight[0] <= j){
                dp[0][j] = value[0];
            }
        }

        //先遍历i,再遍历j,方案一
        for(int i = 1; i < weight.length; i++){
            for(int j = 0; j <= capability; j++){
                if(j < weight[i]){
                    dp[i][j] = dp[i - 1][j];
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }

        //先遍历j,在遍历i,方案二
        // for(int j = 0; j <= capability; j++){
        //     for(int i = 1; i < weight.length; i++){
        //         if(j < weight[i]){
        //             dp[i][j] = dp[i - 1][j];
        //         }else{
        //             dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        //         }
        //     }
        // }

        return dp[weight.length - 1][capability];
    }

三.空间优化

在上面的代码中,最终的代码时间复杂度为O(vn),空间复杂度也为O(vn),(v为背包容量,n为物品数量),在时间复杂度方面,已经很难再优化了,但在空间复杂度方面还有优化的空间。

1.思路

再看看递推关系式dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]),我们真正想要的,是dp[w.length - 1][capacity]这个最后一行的最后一个数。我们想优化空间复杂度,就想到能不能去掉i这个维度,通过控制遍历顺序,达到和二维数组相同的效果呢?经过尝试,我们直接把i的维度去掉,递推关系式变为了dp[j] = min(dp[j], dp[j - w[i]] + v[i]),然后我们想着来控制遍历的顺序来模拟二维数组的数值变化过程。

2.递推顺序

我们是通过控制遍历的顺序来模拟二维数组的数值变化过程。首先,由于公式中可以看到,第i行是由第i - 1行推出来的,所以,i必定是从小到大遍历的。那么,还剩四种情况:

情况1

先遍历i再遍历j,j顺序遍历,也就是如下代码

for(int i = 1; i < nums.lengtd; i++){
    for(int j = 0; j <= capicity; j++){
        //递推代码
    }
}

递推顺序如下所示

1(dp[i - 1][j - w[i]]) (需要的dp[j - w[i]]的位置) 2 3(dp[i - 1][j])(此时dp[j]的位置)(需要dp[j]的位置) 4 5
6(实际dp[j - w[i]]的位置) 7 8(dp[i][j]) (待求的dp[j]位置) ...

可以看出,此时我们需要dp[j - w[i]]的位置在1来求dp[j],但此时dp[j - w[i]]却在6位置,显然不可能求出正确的结果

情况2

先遍历i再遍历j,j逆序遍历,也就是如下代码

for(int i = 1; i < nums.lengtd; i++){
    for(int j = capacity; j >= 0; j--){
        //递推代码
    }
}

递推顺序如下所示

6(dp[i - 1][j - w[i]])
(需要/实际的dp[j - w[i]]的位置)
4 3(dp[i - 1][j])(此时dp[j]的位置)(需要dp[j]的位置) 2 1
... 9(dp[i][j]) (待求的dp[j]位置) 8 7

可以看出,此时我们需要dp[j - w[i]]的位置在6来求dp[j],此时dp[j - w[i]]正好在该位置;需要dp[j]的位置和此时的位置也正好相同,所以可以求出正确的dp[j]的值

情况3

先遍历j再遍历i,j顺序遍历,也就是如下代码

for(int j = 0; j <= capicity; j++){
    for(int i = 1; i < nums.lengtd; i++){
        //递推代码
    }
}

递推顺序如下所示

1(dp[i - 1][j - w[i]]) (需要的dp[j - w[i]]的位置) 5 9(dp[i - 1][j])(此时dp[j]的位置)(需要dp[j]的位置)
2 6 10(dp[i][j]) (待求的dp[j]位置)
3 7 ...
4(实际dp[j - w[i]]的位置) 8

可以看出,此时我们需要dp[j - w[i]]的位置在1来求dp[j],但此时dp[j - w[i]]却在4位置,显然不可能求出正确的结果

情况4

先遍历j再遍历i,j逆序遍历,也就是如下代码

for(int j = capacity; j >= 0; j--){
    for(int i = 1; i < nums.lengtd; i++){
        //递推代码
    }
}

递推顺序如下所示

17(dp[i - 1][j - w[i]])
(需要的dp[j - w[i]]的位置)
13 9(dp[i - 1][j])(此时dp[j]的位置)(需要dp[j]的位置) 5 1
(此时的dp[j - w[i]]还是初始值) 14 10(dp[i][j]) (待求的dp[j]位置) 6 2
15 11 7 3
16 12 8 4

可以看出,此时我们需要dp[j - w[i]]的位置在17来求dp[j],但此时的dp[j - w[i]]还是初始值,显然不可能求出正确的结果

所以遍历顺序应该如情况2所示

3.初始化

这里的初始化有两种方法

  1. 方法1

    类似二维数组的初始化方式,将dp[j]中的每一个数初始化为dp[0][j]的值,i从1开始遍历,具体代码见后面的实现代码部分

  2. 方法2

    初始化为"i = -1"的那一行,定义为任何物品都不装的情况下,容量为j的背包的最大价值,显然最大价值为0,此时i从0开始遍历即可

4.实现代码

  1. 使用方法1初始化

    	public static int bag01(int[] weight, int[] value, int capability){
            int num = weight.length;
            //dp[j]表示在capability为j时,所能获得的最大价值
            int[] dp = new int[capability + 1];
            //方法1初始化
            for(int j = weight[0]; j <= capability; j++){
                dp[j] = value[0];
            }
    
            for(int i = 1; i <= num - 1; i++){
                for(int j = capability; j >= 0; j--){
                    if(j < weight[i]){
                        dp[j] = dp[j];
                    }else{
                        dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                    }
                }
            }
    
            return dp[capability];
        }
    
    
  2. 使用方法2初始化

    	public static int bag01(int[] weight, int[] value, int capability){
            int num = weight.length;
            //dp[j]表示在capability为j时,所能获得的最大价值
            int[] dp = new int[capability + 1];
            //方法2初始化
            //方法2初始化全初始化为0不用动
    
            for(int i = 0; i <= num - 1; i++){
                for(int j = capability; j >= 0; j--){
                    if(j < weight[i]){
                        dp[j] = dp[j];
                    }else{
                        dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                    }
                }
            }
    
            return dp[capability];
        }