背包九讲——由浅入深

背包九讲

作者:一个梦想进入大厂的大学生

动态规划
状态表示
状态计算
集合
属性
集合划分
闫式dp分析法

tips:以下描述都是基于题目而言, 给出思路与代码实现

一、01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0 < N,V ≤ 1000
0 < vi,wi ≤ 1000

题目链接:acwing-2

大意: N个物品,每个物品价值为w[i],一个容积为V的背包,求出能装下货物的最大价值

  • 集合:所有只考虑前i个物体,且总体积不大于j的所有选法
  • 属性:Max
  • 状态计算:状态转移方程,推导过程见下
    在这里插入图片描述计算f[i][j]: 借助f[i-1][j]来进行递推,在考虑第 i 层的时候,我们有两种选择:含第 i 个物品和不含第 i 个物体
    不含第i个物体的时候,显而易见有转移方程:f[i][j] = f[i - 1][j],
    当含第 i 个物体的时候,我们先将第 i 个物体的位置留出来,也就是有转移方程:f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])

    于是我们得出了我们需要的转移方程,代码见下表
//朴素版
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

const int N = 1010;
int v[N], w[N], f[N][N];

using namespace std;

int main()
{
    int N, V; cin >> N >> V;
    for(int i = 1; i <= N; i ++) cin >> v[i] >> w[i];
    
    for(int i = 1; i <= N; i ++)
        for(int j = 0; j <= V; j ++)
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    
    cout << f[N][V];
    
    return 0;
    
}

不难发现,在进行递推的过程中,我们计算第 i 层的时候,只用到了 i - 1 层的数据,所以可以对数组经行空间压缩,将二维数组f[i][j] 压缩为一维f[j]
这里需要注意,当我们得出初始的状态转移方程之后,初始的状态转移方程可能需要很大的空间和时间,我们就需要将初始的状态转移方程进行等效变换,变得尽量使用较少的时间和空间

f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])当我们将第 i 维数直接去掉之后变为
f[j] = max(f[j], f[j - v[i]] + w[i])其实是没有问题的,但是联系到代码后,如下

//朴素
for(int i = 1; i <= N; i ++)
        for(int j = 0; j <= V; j ++)// <----------------
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
//优化后
for(int i = 1; i <= N; i ++)
        for(int j = V; j >= v[i]; j --)// <----------------
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }

注意到代码里面箭头指的代码,这里的遍历顺序发生了改变,因为如果是从小到大遍历,f[j - v[i]]会在本层先被更新,然后又去更新本层的其他f[j]能够压缩空间的本质是每一次的 f 数组都会被更新掉,比如更行到第 i 层的时候,i - 1层的数据就被替换掉,存储就没有意义,于是我们便直接去掉 i 这个维度。观察原转移方程可以发现,后面是第 i - 1 层的f[i - 1][j - v[i]],所以我们更新的时候只能使用上一层的数据来更新本层数据,将其变为从大到小枚举之后,f[j] 一定会在f[j - v[i]]之前被更新掉,换言之就是使用的是第 i -1 层的数据更新的 f[j],就避免了问题的发生,故优化之后的代码为:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

const int N = 1010;
int v[N], w[N], f[N];

using namespace std;

int main()
{
    int N, V; cin >> N >> V;
    for(int i = 1; i <= N; i ++) cin >> v[i] >> w[i];
    
    for(int i = 1; i <= N; i ++)
        for(int j = V; j >= v[i]; j --)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    
    cout << f[V];
    
    return 0;
    
}

二、完全背包问题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000

题目链接:acwing-3

大意:N种物品(无限个),每个物品价值为w[i],一个容积为V的背包,求出能装下货物的最大价值

  • 集合:所有只考虑前 i 物品,且总体积不大于 j 的所有选法
  • 属性:Max
  • 状态计算:状态转移方程,推导如下

在这里插入图片描述
计算f[i][j]时, 可以借助f[i - 1][j]来递推出结果,思维和01背包相似,有下列情况:

  • 含第 i 种物品 0 个
  • 含第 i 种物品 1 个
  • 含第 i 种物品 2 个
  • ……
  • 含第 i 种物品 k 个

    故可以得出转移方程:
    含第i种物品0个:f[i][j] = f[i - 1][j]
    含第i种物品1个:f[i][j] = f[i - 1][j - 1*v[i]] + 1*w[i]
    ……
    含第i个物体k个:f[i][j] = f[i - 1][j - k*v[i]] + k*w[i]
    取上述所有值的最大值即可


    所以容易思考出代码的主要结构,三层循环嵌套即可,代码见下表:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>

using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N][N];

int main()
{
    int n, m; 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 = 0; j <= m; j ++)
            for(int k = 0; k * v[i] <= j; k ++)
            {
                //f[i][j] = f[i - 1][j];
                if(j >= k * v[i]) f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
            
    cout << f[n][m] << endl;
    return 0;
}

明显上述代码的时间复杂度为三次方级别:O(nmk),显然是不满足最优解的。故得出以下将代码优化为两维转移方式:

  • f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i], f[i-1][j-2*v[i]] + 2*w[i],……)
  • f[i][j-v[i] = max(.......f[i-1][j-v[i]], .... f[i-1][j-2*v[i]] + 1*w[i],……)
    很容易发现,两组数据很相似,且因为第二项的j比第一项的j少v[i],所以Max里面的项数也会少一项,所有后面是完美对其的,唯一不同的就是每个数据少了一个w[i],但是对应取最大值不影响,最后加上去即可,最后两式结合得到二维优化的转移方程:
    f[i][j] = f[i-1][j],后f[i][j] = Max(f[i][j], f[i][j-v[i]] + w[i]),代码可以简化为:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>

using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N][N];

int main()
{
    int n, m; 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 = 0; j <= m; j ++)
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i])
                f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
        }


            
    cout << f[n][m] << endl;
    return 0;
}

上述代码的时间复杂度是n2级别,但是可以结合01背包的方式,发现该题目仍然可以从空间的角度优化,比较01背包和完全背包的递推式:

  • 01背包:f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])
  • 完全背包:f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i])

不同点在于完全背包是基于第 i 层,而01背包是基于第 i - 1层,所以这里就不必担心是否本层本更新的问题了,反之如果像01背包一样的逆序遍历,反倒会使用 i - 1来更新 i 层的数据,故这里必须使用升序,代码如下

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>

using namespace std;

const int N = 1010;
int v[N], w[N];
int f[N];

int main()
{
    int n, m; 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;
}

三、多重背包问题

有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000

题目链接:acwing-5

  • 集合:所有只从第 i 个物品中选,且总体积不超过 j 的选法
  • 属性:Max
  • 状态计算:状态转移方程,证明如下

在这里插入图片描述
多重背包问题和完全背包问题极其相似,不同的是完全背包每种物品的数量是无限的,而多重背包每种物品的数量是有s[i]限制的,依然可以使用f[i - 1][j]来递推出f[i][j]

  • 含第 i 种物品 0 个
  • 含第 i 种物品 1 个
  • 含第 i 种物品 2 个
  • ……
  • 含第 i 种物品 s[i] 个
    故和完全背包的朴素做法相似即可,代码如下表
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 110;
int s[N], v[N], w[N];
int f[N][N];

int main()
{
    int n, m; cin >> n >> m;
    
    for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i];
    
    for(int i = 1; i <= n; i ++)
        for(int j = 0; j <= m; j ++)
            for(int k = 0; k <= s[i] && k * v[i] <= j; k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
                
    cout << f[n][m] << endl;
    
    return 0;
}

上述的代码仍然是三维O(nms[i]),显然是有优化空间的,当我们尝试使用完全背包的二维优化方式时,如下

完全背包问题:

  • f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i], f[i-1][j-2*v[i]] + 2*w[i],……)
  • f[i][j-v[i] = max(.......f[i-1][j-v[i]], .... f[i-1][j-2*v[i]] + 1*w[i],……)

多重背包问题:

  • f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i], f[i-1][j-2*v[i]] + 2*w[i],……)
  • f[i][j-v[i]] = max(.......f[i-1][j-v[i]], .... f[i-1][j-2*v[i]] + 1*w[i],……, x)

不难发现,两者的项数是一致的,因为对于第 i 层来讲,一定有放0~s[i]这s[i] + 1种方案,前提是j - v[i] >= s[i]*v[i], 那么必然是有数据能够满足这些条件的。 而为什么完全背包会相差一项呢,因为完全背包物品个数是无限的,那么必然f[i][j]能比f[i][j-v[i]]多考虑一种情况,大一个物品的空间,则上线可以多方一个,故可以将两个式子合并得出一个极简的状态转移方程,但是本题目项数是一致的,所以f[i][j-v[i]]的最后一项f[i][j]没有,所以不能合并,于是该转移方式不能有效的优化代码,下面给出一个新的优化方式

“二进制打包优化法”
比如每个产品的s[i]数量是67,我们可以将该67个一样的产品进行打包分组,使得从打包后的若干组里面进行选择一定能凑出1~67中间的任意一个数,再使用01背包的方式进行处理,下面给出67的分组样例

"1 2 4 8 16 32 4"
很显然,任何一个1~67的数都可以被表示出来,证明理由如下
利用的分组数可以表示的范围
11
1、21~3
1、2、41~7
1、2、4、81~15
1、2、4 、8、161~31
1、2、4 、8、16 、321~63
1、2、4 、8、16 、32、41~67

tips:“可以表示的范围”是用“利用的分组数”组合相加得到的

为什么会出现这样的想法,联系二进制实际存储的方式可以知道,一个数一定有一个自己的二进制表示方式,而我们是将其二进制表示方式里面的不同位的数拆开,比如

  • 20 = 1
  • 21 = 2
  • 22 = 14
  • ……
  • 2n = …

当我们将某个数拆成这样的若干个数之后,会出现拆下去的情况,这时候我们只需要将多余的数拿出来,单独打包即可比如上面67最后的4无法再拆解,直接将4进行打包,则凭借所有的包组合出1~67里面的数据,下面给出代码实现

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

//数据范围是2000,拆成二进制有11次方,故需要22000,这里开25000,但是开了25000就一定要用优化版的01背包问题
const int N = 25000;
int s[N], v[N], w[N];
int f[N];

int main()
{
    int n, m; cin >> n >> m;
    
    int cnt = 0;
    for(int i = 1; i <= n; i ++)
    {
        //将每种物体拆分开形成能组合成任意小于等于本身组数的集合v w s
        int a, b, c; cin >> a >> b >> c;
        
        int mul = 1;
        while(c > mul)
        {
            cnt ++;
            v[cnt] = mul * a;//这里是将物品打包好之后再放入数组
            w[cnt] = mul * b;
            c -= mul;
            mul *= 2;
        }
        if(c > 0)//最后可能会多出一部分没有办法继续分解,直接打包加入数组
        {
            cnt ++;
            v[cnt] = c * a;
            w[cnt] = c * b;
        }
        
    }
    
    //01背包模板
    for(int i = 1; i <= cnt; 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 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100

题目链接:acwing-9

  • 集合:只从前 i 个组里面选,且总体积不超过j
  • 属性:Max
  • 状态计算:状态转移方程,证明如下:

在这里插入图片描述
每组最多只能选择一个,这样的依然可以从 i - 1 的维度推出 i 维度,转移方程如下
f[i][j] = max(f[i-1][j], f[i-1][j-v[i][k]] + w[i][k], ……)

这里的 k 是指每一组里面的每一种物品,只能选择一个,就在 i 组里面挑一个,选出最优,在存储的时候我们用 s 数组来存储每组里面物品的个数,便于遍历,我们只需要在01背包问题的基础上加一个维度来遍历每一组里面的所有物品即可,这里是三重循环,因为每组里面的物品价值不一样, 数量不一样,所以必须要遍历到每一个物品,这里没有办法像完全背包一样找递推关系优化到二维时间,朴素版代码如下:

代码如下:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 110;
int v[N][N], w[N][N], s[N];
int f[N][N];

int main()
{
    int n, m; cin >> n >> m;
    
    for(int i = 1; i <= n; i ++)
    {
        cin >> s[i];
        //第i类物品,有k个
        for(int j = 1; j <= s[i]; j ++)
            cin >> v[i][j] >> w[i][j];
    }
    
    for(int i = 1; i <= n; i ++)
    {
        for(int j = 0; j <= m; j ++)
        {
            f[i][j] = f[i-1][j];//不选
            for(int k = 1; k <= s[i]; k ++)
            {
                if(j >= v[i][k]) f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);    
            }
        }
    }
    
    cout << f[n][m] << endl;
    
    return 0;
}

仔细发现,这里在第 i 层也只用到 i - 1 层的数据,所以可以像01背包一样逆向枚举 j 即可,空间优化版代码如下:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 110;
int v[N][N], w[N][N], s[N];
int f[N];

int main()
{
    int n, m; cin >> n >> m;
    
    for(int i = 1; i <= n; i ++)
    {
        cin >> s[i];
        //第i类物品,有k个
        for(int j = 1; j <= s[i]; j ++)
            cin >> v[i][j] >> w[i][j];
    }
    
    for(int i = 1; i <= n; i ++)
    {
        for(int j = m; j >= 0; j --)//下文有注释
        {
            for(int k = 1; k <= s[i]; k ++)
            {
                if(j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);    
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

代码注释:这里与其他背包问题不同,其他背包问题的每组里面的价值已知,所以只需要从w[i] ~ m,因为即使遍历了小于w[i],也会被if给划掉, f[i - w[i]]的下标不能小于0 ;
这里每个价值不一样,可能为0,可能为1,没有确定的 " w[i] " 所以不确定到底下标什么时候小于0,所有统一从m~0, 然后给出if判断


五、混合背包问题

有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
si=−1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
−1≤si≤1000

题目链接:acwing-7

  • 集合:只从前 i 个组里面选,且总体积不超过j
  • 属性:Max
  • 状态计算:状态转移方程推导如下
    在这里插入图片描述

由题意知,有三种物品:

  • 最多使用一次(01背包问题)
  • 最多使用s[i]次(多重背包问题)
  • 最多使用无线次(完全背包问题)

于是当我们思考这个问题的时候,可以直接递推加上三重判断分类来分析问题,对于每个物品来说,都需要判断它所属的是哪一种背包问题,然后用对应的状态转移方程来得到当前物品的最优策略,但是我们不难思考到,当我们对其中的多重背包问题进行决策的时候,我们采取的方法是将其进行二进制分组优化,转化为01背包问题来解决,但是当我们只讨论多重背包问题的时候,我们会遇到一个瓶颈,二进制分组优化之后的物品数量变多,会变得相当麻烦,所以我们可以在读入给时候就将多重背包问题拆分后读入数组,这样三种背包问题就只剩下两种,为01背包问题和完全背包问题。当我们数据拆分读取完毕之后,开始递推的时候,因为我们对于每个物品来讲都是独立的,基于之前所有最优决策的,所以仍然可以采用 i 层或 i-1 层来得到 i 层,这也就是为什么可以在最底层循环直接分类递推的原因。 代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>

using namespace std;

//范围是1000,换成二进制是10,也就是十倍,至少需要10000,这里开15000
const int N = 15000;
struct Things{
    int s;
    int v, w;
};
int f[N];

vector<Things> things; 

int main()
{
    int n, m; cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        int a, b, c; cin >> a >> b >> c;
        if(c < 0) things.push_back({-1, a, b});
        else if(c == 0) things.push_back({0, a, b});
        else
        {
            //这里是多重背包,需要分组存放
            int k = 1;
            while(c > k)
            {
                things.push_back({-1, k * a, k * b});
                c -= k;
                k *= 2;
            }
            if(c > 0) things.push_back({-1, c * a, c * b});
        }
    }
    
    //现在来分情况讨论,有的是01背包问题,有的是完全背包问题,分两组讨论
    for(auto thing : things)
    {
        if(thing.s == -1)//01背包问题
        {
            for(int j = m; j >= thing.v; j --) f[j] = max(f[j], f[j - thing.v] + thing.w);
        }
        else//完全背包问题
        {
            for(int j = thing.v; j <= m; j ++) f[j] = max(f[j], f[j - thing.v] + thing.w);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

但是我们仔细发现可知,所谓的完全背包问题也受到一个条件所限制,虽然是无限个,但是是取不到无限个的,因为背包体积上限不允许也不可能取到当前物品的无限个,所以我们可以将其无限个改为背包容积所允许的上限个,这也就将完全背包问题转化为多重背包问题,根据上文可知,多重背包问题的一个优化方式是二进制分组优化,这样分组之后其实是将一个多重背包问题转化为01背包问题,故我们在读入数据的时候可以将三种背包问题全部转化为01背包问题,代码实现如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>

using namespace std;

//范围是1000,换成二进制是10,也就是十倍,至少需要10000,这里开15000
const int N = 15000;
struct Things{
    int s;
    int v, w;
};
int f[N];

vector<Things> things; 

int main()
{
    int n, m; cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        int a, b, c; cin >> a >> b >> c;
        if(c < 0) things.push_back({-1, a, b});
        else//这里是完全背包问题和多重背包问题,但是因为背包容积优先,所以可以转化为多重背包问题
        {
            if(c == 0) c = m / a;//如果是完全背包问题,那么按照最多能存储的数量来存
            
            //这里是多重背包,需要分组存放
            int k = 1;
            while(c > k)
            {
                things.push_back({-1, k * a, k * b});
                c -= k;
                k *= 2;
            }
            if(c > 0) things.push_back({-1, c * a, c * b});
        }
    }
    
    //现在全部都是01背包问题,直接用01背包问题的状态转移方程
    for(auto thing : things)
        if(thing.s == -1)//01背包问题
            for(int j = m; j >= thing.v; j --) f[j] = max(f[j], f[j - thing.v] + thing.w);

    
    cout << f[m] << endl;
    
    return 0;
}


六、二维费用的背包问题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。

输出最大价值。

输入格式
第一行两个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000

题目链接:acwing-8

  • 集合:只从前 i 个组里面选,且总体积不超过j
  • 属性:Max
  • 状态计算:状态转移方程推导如下:

在这里插入图片描述
二维费用的背包问题实质上和01背包问题很像,就是多了一个维度,朴素版的01背包问题是二维空间,故朴素版的二维费用的背包问题是三维空间,但是不难发现,这里也是利用 i - 1层来递推得到i层,故也可以将空间复杂度优化,代码和01背包问题极度相似,值得注意的是需要对应清楚哪一维度代表的是什么意思,比如我的代码 f 数组第一维是表示体积,第二维是表示重量,在后面递推的时候需要三层循环遍历,体积和重量都需要全部枚举一遍,代码如下:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 1010;
int V[N], W[N], money[N];
int f[N][N];//左边是体积,右边是重量

int main()
{
    int n, m, w; cin >> n >> m >> w;//件数,容积,最大重量
    
    for(int i = 1; i <= n; i ++) cin >> V[i] >> W[i] >> money[i];
    
    for(int i = 1; i <= n; i ++)
        for(int j = m; j >= V[i]; j --)//容积
            for(int k = w; k >= W[i]; k --)//重量
                f[j][k] = max(f[j][k], f[j - V[i]][k - W[i]] + money[i]);
                
    cout << f[m][w] << endl;
    
    return 0;
}

更新尚未完全,仍在学习中,欢迎批评指正!

qq:2166364824

  • 9
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值