0-1背包的二维和一维dp数组

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

背包问题,最难的就是初始化和遍历顺序!

二维dp数组01背包

// 0-1背包问题
// 二维dp数组
// n个物品 背包容量为m
int knapSack(int n, int m, vector<int>& weight, vector<int>& value) {
    vector<vector<int> dp(n+1, vector<int>(m+1, 0));
    // dp[i][j]:从前i个物品中选择放入容量为j的背包中得到的最大价值
    // 注意这种定义,第i件物品的重量为weight[i-1],价值为value[i-1]
    // dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
    // 初始化
    // 当j=0时,背包容量为0,最大价值为0;当i=0时,也就是前0件物品,也就是没有物品,最大价值也是0
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (j - weight[i-1] < 0) // 如果当前背包容量放不下第i件物品,那么前i件物品放入背包得到的最大价值就是前i-1件物品放入获得的最大价值
                dp[i][j] = dp[i-1][j];
            else { // 如果能放下,从放和不放两种选择里取最大值
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1]);
            }
        }
    }
    return dp[n][m];
}

为什么两个for循环的嵌套顺序这么写?反过来行不行?

1.先遍历物品和先遍历背包容量其实都可以,但是先遍历物品更好理解。

2.反过来也是可以的,这跟递归的本质和递推的方向有关系。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i-1]] + value[i-1]);递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i-1]]推导出来的,他们俩都在dp[i][j]的左上角方向(包括正左和正上两个方向),分析先遍历物品和先遍历背包的过程,在计算dp[i][j]之前都已经得到了,不影响公式的推导!

初始化的逻辑

根据dp数组的定义,dp[i][j]:从前i个物品中选择放入容量为j的背包中得到的最大价值,那么从前0个物品中选择放入,也就是没有物品,最大价值是0;背包容量为0的时候,最大价值为0,所以初始化为0就可以了。但是要注意数组的初始化,因为价值是求最大的,如果所有物品的价值都是大于0的,那数组初始化为0没问题,如果价值有负数,必须初始化为负无穷才行。

一维dp数组01背包

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i-1]] + value[i-1],可以看出dp[i][j]这一层的推导只跟上一层dp[i-1][]有关系,所以其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i-1]] + value[i-1]);于其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j]。

dp[j]:容量为j的背包能装的最大价值

dp[j]可以通过dp[j - weight[i-1]]推导出来,dp[j - weight[i-1]]表示容量为j - weight[i-1]的背包所背的最大价值。

dp[j - weight[i-1]] + value[i-1] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j],一个是取dp[j - weight[i-1]] + value[i-1],指定是取最大的,毕竟是求最大价值。

所以,递推公式为:dp[j] = max(dp[j], dp[j - weight[i-1]] + value[i-1]);

初始化时候跟定义吻合:容量为0的时候,能装的最大价值dp[0] = 0,其他位置的初始化也就是数组的初始化,跟二维一样的考虑,跟价值正负有关。

// 0-1背包问题
// 一维dp数组
// n个物品 背包容量为m
int knapSack(int n, int m, vector<int>& weight, vector<int>& value) {
    vector<int> dp(m+1, 0);
    // 当j=0时,背包容量为0,最大价值为0
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 1; j--) {
            if (j - weight[i-1] >= 0) {
                dp[j] = max(dp[j], dp[j-weight[i-1]] + value[i-1]);
            }
        }
    }
    return dp[m];
}

或者可以优化为(主要是容量遍历条件优化):

// 0-1背包问题
// 一维dp数组
// n个物品 背包容量为m
int knapSack(int n, int m, vector<int>& weight, vector<int>& value) {
    vector<int> dp(m+1, 0);
    // 当j=0时,背包容量为0,最大价值为0
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= weight[i-1]; j--) {
            dp[j] = max(dp[j], dp[j-weight[i-1]] + value[i-1]);
        }
    }
    return dp[m];
}

需要注意一维dp数组的遍历顺序和二维dp数组的遍历顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

倒叙遍历是为了保证物品i只被放入一次!

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

二维dp数组不用倒叙的原因是:dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。

一维数组的0-1背包,两个for循环的顺序反过来写行不行?为什么?

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

所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!

posted @ 2021-02-05 18:47  不妨不妨,来日方长  阅读(585)  评论(0编辑  收藏  举报