再来一遍背包问题 | 01背包

139 阅读5分钟

哈喽哈喽这里是小菜不拖延博主,本篇文章主要是重温一下01背包问题(实在是忘记了,又老是遇到),重新温习又有一些疑惑的地方。大佬可以划走了~希望可以帮助到你(本篇主要基于acwing进行学习)

原题连接:2. 01背包问题 - AcWing题库

image.png

二维基础代码

  • 情况一:不选第i个物品,价值就是前i-1物品时的价值
  • 情况二:剩余空间充足时,取选和不选时的最大价值
#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;

//n为物品数量,m为总体积
int n,m;
//v是体积,w是价值
int v[N],w[N];
//这里声明变量之后值都默认为1,f:选取i个物品,体积为j时的最大价值
int f[N][N];

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    //这里i的含义是每次选取前i个物品,比如i=1,选到第1个物品(包括1)
    for(int i=1;i<=n;i++)
    {
        //选取前i个物品,体积为j时,总价值,j不能超过m总体积
        for(int j=0;j<=m;j++)
        {
            //情况一:不选第i个物品,价值为前i-1个时的值,
            f[i][j]=f[i-1][j];
            
            //情况二:假如剩余体积够,可选当前物品,比较选与不选时的最大价值
            //如何理解f[i-1][j-v[i]]+w[i]?
            //比如现在是第2件物品,那么我们是在选了前一件物品的情况下,再选第二件
            //也就是f[2-1](第1件物品)[j-v[i]](选了第二件,所以减去第二件的体积),最后加上第二件的价值
            //如果你觉得应该是:f[i][j-v[i]]+w[i],那就是你默认已经选了第i件了,在选了第i件物品上,又减去他的体积,显然是不合理的,你都选过了为什么现在还要减去这个
            if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
            //也可以写成
             if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}
function knapsack(n, m, items) {
    let f = Array(n+1).fill().map(() => Array(m+1).fill(0));
    for (let i = 1; i <= n; i++) {
        for (let j = 0; j<=m; j++) {
            f[i][j]=f[i-1][j]
            if(j>=items[i][0]) f[i][j] = Math.max(f[i-1][j], f[i-1][j - items[i][0]] + items[i][1]);
        }
    }
    
    return f[n-1][m];
}

// 示例输入
let n = 4;
let m = 5;
let items = [
    [0,0],
    [1, 2],  // 物品1: 重量 1,价值 2
    [2, 4],  // 物品2: 重量 2,价值 4
    [3, 4],  // 物品3: 重量 3,价值 4
    [4, 5]   // 物品4: 重量 4,价值 5
];

console.log(knapsack(n, m, items));  // 输出最大价值

疑惑点总结

为什么是f[i-1][j-v[i]]+w[i],而不是f[i][j-v[i]]+w[i]

比如现在是第2件物品,那么我们是在选了前一件物品的情况下,再选第二件,也就是f[2-1] (第1件物品)[j-v[i]] (选了第二件,所以减去第二件的体积),最后加上第二件的价值。如果你觉得应该是:f[i][j-v[i]]+w[i],那就是你默认已经选了第i件了,在选了第i件物品上,又减去他的体积,显然是不合理的,你都选过了为什么现在还要减去这个

优化一维代码

#include<iostream>
#include<algorithm>
using namespace std;
 
const int N=1010;
 
int n,m;
int v[N],w[N];
int f[N];
 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];//体积与价值
    for(int i=1;i<=n;i++){
        for(int j=m;j>=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m];
  

    return 0;
}
function knapsack(n, m, items) {
    let f = Array(m + 1).fill(0);
    for (let i = 1; i <= n; i++) {
        let [weight, value] = items[i];
        for (let j = m; j >=weight; j--) {
            f[j] = Math.max(f[j], f[j - weight] + value);
            console.log(f,j,weight)
        }
    }

    return f[m];
}

// 示例输入
let n = 4;
let m = 5;
let items = [
    [0,0],
    [1, 2],  // 物品1: 重量 1,价值 2
    [2, 4],  // 物品2: 重量 2,价值 4
    [3, 4],  // 物品3: 重量 3,价值 4
    [4, 5]   // 物品4: 重量 4,价值 5
];

console.log(knapsack(n, m, items));  // 输出最大价值

二维到一维理解

其实就是等于去掉一层:f(i)[j]=max(f(i)[j],f(i)[j-v[i]]+w[i]);

ok此时我们就会发现,假如他们都是i,一起减去没有问题,可是原本的代码是f[i-1]不是f[i],所以直接去掉等于又重新基于i的状态减去了i的体积(参考上述疑惑点总结的位置),也就是这个物品被选了不止一次,所以不是对的

这里更改了体积的遍历,变成从大到小遍历,j-v[i]一定小于j,所以j-v[i]一定没有被算过

为什么从二维到一维就需要,体积从大到小遍历?

从运行结果来理解

其实盯着代码看很难理清楚的话,最好的方法就是先利用打印搞清楚原理(是的我就是consolelog的忠实教徒),首先我们可以看到二维数组时,我们从左到右,依次确立结果

二维数组f的输出: image.png

一维数组按照体积从小到大时f的输出:

 for (let i = 1; i <= n; i++) {
     let [weight, value] = items[i];
     for (let j = 0; j <= m; j++) {
         if(j>=weight) f[j] = Math.max(f[j], f[j - weight] + value);
         console.log(f,j,weight)
     }
 }

image.png

一维数组按照体积从大到小时f的输出:

image.png

每次我们都要保证j-v[i]这个状态没有被算过,否则观察第二图,当被算过时也就是这个物品被拿过,就会重复叠加,无法得出正确结果。

原来的二维逻辑是,始终坚持i来自于i-1,i-1这一部分没有被算过,现在化为一维数组,没有被选过的任务给到了j-v[i],j来自于j-v[i],j>j-v[i]. 如下图,正序计算会出现j-v[i]被计算过,也就是被拿过的情况,和我们的出发点是不一样的。 d33bbfd67e696bf41e3ec830e3a09bc.jpg

练习题目