背包问题合集

背包问题

01背包问题

题意:

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

思路:
二维数组思想:

通过将前 i 个物品放入进容量为j 的背包中

可以使用表格表示(二维数组思想)

在这里插入图片描述

1、将2顺延,因为只能够装下容量为1的物品

2、容量为2,使用2个物品时,可以装下第二个物品和容量为0时的物品,比较f[1][2]和第二个物品+f[0][0]的价值,选择后者在这里插入图片描述

3、容量为3,可以装下第二个物品和容量为1时的物品,比较f[1][3]和第二个物品+f[1][1]的价值,选择后者

4、容量为1,此时选择第3个物品的容量为3,装不下,顺呈f[2][1]

5、容量为3,可以装下第3个物品,此时比较f[2][3]f[2][0]+第三个物品价值4, 选择前者

6、容量为5,比较f[2][5]和第3个物品+f[2][2]的价值,选择后者

一维数组思想:

与二维数组思想一致,我们每一次比较都是比较上一位(未加上第i个物品时,容量为j)的价值和上一位(未加上第i个物品时,容量为j - v[i]),故只需要将数组从后往前遍历就可以做到(此处讲一下为什么从后遍历:如果从前遍历的话,相当于我们使用的是加上了第i 个物品时,容量为j - v[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][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 = 1; j <= m; j ++ ) {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 比较【未使用i物品但使用容量为j】和【未使用i物品但使用容量为j-v[i]加上i物品的价值 】
        }
    }
    cout << f[n][m] << endl; // 输出的是将所有容量和物品都用完的最大价值
}

一维:

#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] << endl;
    return 0;
}

完全背包问题

题意:

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

思路:

因为每一个物品都有无穷多个,所以我们每一次添加时都需要判断是否继续加i这个物品

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s0oLd8po-1690626499782)(C:\Users\李旭云\Pictures\Camera Roll\image-20230729163246441.png)]

样例解析:

使用第i个物品时可以将容量为1~5的所有情况都给表示为最大值,如图

因为样例无法解释思路中的特殊情况,故将样例的数值改了一个

改后解析:

首先所有的f值状态只和第1个物品有关(状态如图)

图中步骤:

1、在可以使用第2个物品时,容量为2,此时f[2]的价值为f[0] + w[2],比原来的f[2]大,所以发生改变

2、由f[2] 到 f[4]的改变,因为在遍历到f[4]时,f[2]已经考虑到了第二个物品的影响,换而言之,就是此时的f[2]是容量为2,使用到两个物品时的最大值,由这个状态加上w[2]与原来的f[4]进行比较

3、同2

4、(可能图中看不清,即i = 2的f[2]到i = 3的f[2])因为装不下容量为3的物品,所以不需要改变状态,图中的粗线是将f的状态顺延

代码块:
#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 = v[i]; j <= m; j ++ ) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    
    cout << f[m] << endl;
    return 0;
}

多重背包问题I

题意:

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

思路:

朴素做法:

在01背包的基础上再加上一重循环,进行判断s[i]个该物品,上代码发现区别

代码块:
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
    for(int i = 1; i <= n; i ++ ) { // 第一重循环,对前 i 个物品进行放置
        for(int j = 0; j <= m; j ++ ) { // 第二重循环,对 j 个空间进行放置
            for(int k = 0; k <= s[i] && k * v[i] <= j; k ++ ) { // 第三重循环,对放 k 个 i 物品进行筛选
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k); 
            }
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}

多重背包问题II

题意:

与多重背包I的题意相同,但是数据更严格,需要使用优化过后的算法

思路:

思想:通过二进制想法,将一个拥有很多的物品分解为logn个物品进行处理,这logn个物品可以通过组合达到满足任意个该物品的数量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ozykOQd0-1690626499782)(C:\Users\李旭云\Pictures\Camera Roll\image-20230729165919888.png)]

如果数字过大如130:

分解为1,2,4,8,16,32,64,3即可获得所有1~130所有个数的组合

有了这个思想之后,问题就再一次变成了01背包问题

代码块:
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 12010, M = 2010; // N = n * logn

/* 思想:通过二进制想法,将一个拥有很多的物品分解为logn个物品
进行处理,这logn个物品可以通过组合达到满足任意个该物品的数量*/

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;
    
    int cnt = 0; // 下标
    for(int i = 0; i < n; i ++ ) {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s)  
        {
            v[cnt] = k * a;
            w[cnt ++ ] = k *b;
            s -= k;
            k *= 2;
        }
        if(s > 0) {
            v[cnt] = s * a;
            w[cnt ++ ] = s *b;
        }
    }
    n = cnt;
    for(int i = 0; i < n; i ++ ) { // 01背包思路
        for(int j = m; j >= v[i]; j -- ) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

分组背包问题

题意:

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

思路:

思想:

根据上一次的f(i)去加上该组的物品,从大容量开始装该组的物品,只装一个,直到更新所有的f(i) ,因为一组的物品只能使用一次,所以要从后往前遍历使用上一次(还未使用i组)的状态,具体下文:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uaYPVuz-1690626499782)(C:\Users\李旭云\Pictures\Camera Roll\image-20230729172001870.png)]

样例解析:

操作:

1、该步比较上一次的f(5)【i = 1, j = 5】和上一次的f[5 - v[k]] + 第2组的w[k],可以发现比较了一次(比较次数和组内的物品个数相同),这一步比较了两个数:

第一个:i = 1操作的f[5]

第二个:i = 1操作的f[2] + 第2组的4

明显第二个> i = 1 的 f[5]

2、因为第二组中最小的体积为3, 在使用容量为2的情况下无法使用,故顺延

3、比较的是i = 2操作的f[1] + 5 和i = 2 操作的f[5],7 < 8,故顺延

总结到此可以发现,我们每一次的变化都只使用到了改组的上一次操作和该组的第k个成员,因此,每一次操作我们都只会使用 i 组的一个成员

代码块:
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N]; // v,w 表示第几组的第几个物品的体积和价值 , s 表示的是组
int f[N]; // 容量为j 可放的最大价值

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++ ) 
    {
        cin >> s[i];
        for(int j = 0; j < s[i]; j ++ ) {
            cin >> v[i][j] >> w[i][j];
        }
    }
    
    for(int i = 1; i <= n; i ++ ) { // 第 i 组
        for(int j = m; j >= 0; j -- ) { // 容量为 j
            for(int k = 0; k < s[i]; k ++ ) { // 该组第 k 个
                if(v[i][k] <= j) {
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
                }
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

总结

通过比较四种背包问题,可以发现,我们需要通过不同的题目意思去使用不同的操作方法,类如:

01背包中,每个物品只有一个,我们的状态随着使用物品的增多而改变,改变方式就是通过上一次的状态(容量 j 减去该物品的容量)加上这个物品的价值和上一次的状态(容量为 j ),状态方程可以写成f[j] = max(f[j], f[j - v[i]] + w[i]),从上一个状态变化,因此我们是从后往前遍历。

完全背包问题中,每一个物体都可以使用无限次,我们的状态就随着使用物品增多而改变的同时,还需要考虑该物品使用多次,改变方式就是通过使用的空间为 j ,找到如果多次使用该物品那么往前寻找到使用空间为j - v[i]的 f[j - v[i]] 状态加上w[i]和原来的f[j]比较,f[j] = max(f[j], f[j - v[i]] + w[i]),因为我们是需要考虑多个该物品,因此我们是从前往后遍历。

多重背包问题中,我们需要将多重背包问题的出现s次的物品分解为几个部分(部分的要求就是可以通过不同的组合组合出1~s的任意一种组合),然后再通过01背包思想去求解。

分组背包问题中,每一组物品都只能只用一次,同时不能同时使用该组2个以上物品,那么我们需要通过上一次的状态去从后往前遍历这一次状态,才可以满足该组该物品在该状态使用次数为1次或者不适用(即顺延),可以理解为每一组状态如果都只是用了该组的一个物品,那么下一组遍历产生的 f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);,同样,由于只能使用一次,我们是从后往前遍历。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值