还有人学不会背包问题模板么?


写在最前面:学算法,就上Acwing, 感谢Y总。

背包问题

所有的DP问题都要归结成2点:

  • 如何表示状态:如果使用二维数组f[i][j]来表示状态,需要考虑清楚i,j表示什么,即每个状态的含义时什么。需要几维来表示一个状态。数组f[][]表示的属性是什么?通常属性有max, min, count三种。
  • 状态的计算: 如何一步步推导出最后的计算结果。

01背包问题

给定N个物品,它们的体积是 V i V_i Vi, 价格是 W i W_i Wi每个物品只能使用一次(可以不用)

我们需要找一些物品,它们的体积之和是小于背包容量的,求能够获得的最大价值是多少?

例题:

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

第 i件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

接下来有 N行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 ii 件物品的体积和价值。

输出格式

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

数据范围

0<N,V≤1000,
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
解答与思路:

解答最开始的两个问题:

如何表示状态:使用一个二维数组f[i][j]来表示状态,其中i表示从1~i的物品中选择,不涉及大于序号i的物品;j表示物品的总体积不超过jf[i][j] = x, x表示当从1~i个物品中挑选,并且总体积不超过j的最大价值是多少,显然是max属性。

如何进行推导?

我们可以把任何一个状态f[i][j]分成两个部分:

  1. 如果这个max状态里,不包含第i个物品,那么f[i][j] = f[i-1][j];
  2. 如果这个max状态,包含第i个物品,那么先排除掉这个物品,再加上这个物品f[i][j] = f[i-1][j - v[i]] + w[i]

所以结合在一起,取最大值,f[i][j] = max(f[i-1][j] , f[i-1][j-v[i]] + w[i] ).

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;
// n 表示n件物品,m表示背包的总容积
int n,m;
// v来存放不同物品的体积,w来存放不同物品的价值
int v[N], w[N];
// f来存储状态
int f[N][N];

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
        cin >> v[i] >> w[i];
    }
    // 由于f定义在全局变量中,初始化各个值都为0
    // 当i=0,表示从没有物品中选,f的值一定都是0
    for(int i = 1; i <= n; i++){
        for(int j = 0; j <= m; j ++){
            // 即当不选第i个物品时
            f[i][j] = f[i-1][j];
            // 当背包空间足够放第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;
}

接下来,我们又发现,f[i][j]的变化,只与f[i-1]层有关系,和之前的层都没有关系,所以可以用滚动数组做。

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;
// n 表示n件物品,m表示背包的总容积
int n,m;
// v来存放不同物品的体积,w来存放不同物品的价值
int v[N], w[N];
// f来存储状态
int f[N][N];

int main(){
    cin >> n >> m;
    // for loop start at 1, which will be convient for dynamic programming.
    for(int i = 1; i <= n; i ++){
        cin >> v[i] >> w[i];
    }
    // 由于f定义在全局变量中,初始化各个值都为0
    // 当i=0,表示从没有物品中选,f的值一定都是0
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--)
 //     for(int j = v[i]; j <= m; j ++){        
            // 即当不选第i个物品时
            // f[j] = f[j]; 恒等式,所以可以删掉
            
            // 当背包空间足够放第i个物品时
            // if( j >= v[i])  f[i][j] = max(f[i][j], f[i - 1][ j - v[i]] + w[i]); 由于j >= v[i], 循环才有意义,
            // 所以可以更改循环的初始值 为 j = v[i], 但是,又因为j - v[i] < j的, 如果直接删去第一维,那么f[j-v[i]] = f[i][j-v[i]],是已经被计算过的。
            // 但是实际上我们计算的是f[i-1][j-v[i]], 如果我们倒着遍历j,那么计算f[j-v[i]] + w[i]时,f[j-v[i]]的数值是原来上一层的数据
            f[j] = max(f[j], f[j - v[i]] + w[i]); 
            
        }
    }
    cout << f[m] << endl;
    return 0;
}

二维费用的背包模板问题及其变形

模板题:https://www.acwing.com/problem/content/description/8/

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值是 w i w_i wi

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

输入格式

第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N 行,每行三个整数 v i , m i , w i v_i,m_i,w_i vi,mi,wi用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式

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

数据范围

0 < N ≤ 1000 0 < V , M ≤ 100 0 < v i , m i ≤ 100 0 < w i ≤ 1000 0<N≤1000 \\ 0<V,M≤100\\ 0<v_i,m_i≤100\\ 0<w_i≤1000\\ 0<N10000<V,M1000<vi,mi1000<wi1000

输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8
直接上压缩空间后的版本:
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;
int n, v, m;
int f[N][N];
int main(){
    cin >> n >> v >> m;
    for(int i = 0; i < n; i ++){
        int a, b, c;
        cin >> a >> b >> c;
        for(int j = v; j >= a; j --){
            for(int k = m; k >= b; k --){
                f[j][k] = max(f[j][k], f[j - a][k - b] + c);
            }
        }
    }
    cout << f[v][m] << endl;
    return 0;
}

完全背包

特点:每件物品有无限个,只要能拿可以随便拿。

例题:

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

第 i 种物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

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

数据范围

0<N,V≤1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
解答与思路:

关于“如何表示状态?”这个问题,与01背包问题一致,都是用两维数据来表示当前的状态,并且数组的属性也是Max属性。

区别在于状态的计算上:

由于完全背包问题的特点是,物品数量的无限的,只要体积可以拿就行;所以我们可以将状态f[i][j]拆分:

在这里插入图片描述

把第i个物品拆分成:拿取0次,拿取1次,拿取2次… 拿取k次;

如k=0,则f[i][j] = f[i-1][j], 遍历k,得到的公式是f[i-1][j-k*v[i]] + k * w[i].

朴素解法的f[i][j] = max(f[i-1][j-k*v[i] + k * w[i])

O ( n 3 ) O(n^3) O(n3)的暴力做法,会导致TLE

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
int n,m;

int f[N][N];
int v[N], w[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 = 0; j <= m; j ++){
            // 下面这个是 j - k *v >= 0
            for(int k = 0; k * v[i] <= j; k++)
                // 注意这里是max()函数内是f[i][j], 不是f[i-1][j],因为当k = 0的时候,就包含了不选第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;
}

优化时间复杂度的解法, 将三维降低为二维:

我们可以把上面朴素做法的公式拆开:

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

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

通过上下两式进行一个比较,可以得到:

f[i][j] = max(f[i-1][j], f[i][j-v] + w)

因此可以把第k所在的维度消去。

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
int n,m;

int f[N][N];
int v[N], w[N];
int main(){
    cin >> n >> m;
    
    // 背包问题都可以先从i=1开始
    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][j], f[i][j-v[i]] + w[i]);
        }
    }
    
    cout << f[n][m] << endl;
    
    return 0;
}

继续优化空间的解法:

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
int n,m;

int f[N];
int v[N], w[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-v[i]]实际上是f[i][j-v[i]],是同一层的,所以
            // 不能将循环倒过来计算
            f[j] = max(f[j], f[j-v[i]] + w[i]);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

多重背包

每个物品的数量有限制,最多能拿多少个。

例题:

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

第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi

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

输入格式

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

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

输出格式

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

数据范围

0<N,V≤100
0 < v i , w i , s i ≤ 100 0<v_i,w_i,s_i≤100 0<vi,wi,si100

输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
解答与思路

同样是解决DP问题,从两个问题开始:

状态如何表示:所有的背包问题都用二维数组来表示状态集合,f[i][j], 表示从前i件物品中,拿出体积不超过j的所有物品; 属性是max属性。即f[i][j]中存储的是价值的最大值。

状态的计算:

与完全背包类似,也是将状态可以分成‘k’份 ,f[i][j] = max(f[i-1][j - k * v[i]] + k * w[i])

这里的k与完全背包不同的是,k有限制: k * v[i] <= j && k <= s[i]

  • 朴素版本的解题为:

这是因为数据量,大约是 O ( 1 0 6 ) O(10^6) O(106)级别,所以能通过。

#include<bits/stdc++.h>

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];
    }
    
    // 时间复杂度约为O(n^3)
    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++){				// 这一行的condition要注意
                f[i][j] = max(f[i][j], f[i-1][j - v[i] *k] + k * w[i]);
            }
        }
    }
    
    cout << f[n][m]<< endl;
    
    return 0;
}
  • 如果数据量进一步增大,为以下数据:

0 < N < = 1000 0 < V < = 2000 0 < v i , w i , s i < = 2000 0<N<=1000\\ 0<V<=2000\\ 0<v_i,w_i,s_i<=2000 0<N<=10000<V<=20000<vi,wi,si<=2000

如果还按照上面的做法,时间复杂度约为O(10^9), 会超时;

所以需要采用别的方法,降低时间复杂度: 二进制优化打包法。

在第三层for循环里 for(int k = 0; k <= s[i] && k *v[i] <= j; k++){}, 是枚举了每一个物品的每一项。如果我们将每一个物品的个数按照二进制的方式打包, 比如某个物品总共有11个,那么将其分为1 + 2 + 4 + 4; 这样就只需要枚举4次,原来需要枚举12次(0 ~ 11);大大减小了枚举次数,而二进制在正整数集上又是完备的,所以这种优化方法非常合理。

将一层循环中的O(n)优化成O(logn)形式;

很多枚举物品类型的问题都可以尝试思考二进制打包的方法来优化。

又比如一个物品总共有200个,可以将其打包成:1,2,4,8,16,32,64,73(不能选128,否则凑不出200);

一个数S,将其划分成: 1 , 2 , 4 , 8 , 16 , 2 k , c ; 1, 2, 4, 8, 16, 2^k,c; 1,2,4,8,16,2k,c;

#include<bits/stdc++.h>

using namespace std;

// 这里的N需要计算: N * 12 + 10; 12是因为2000在2^10 ~ 2 ^ 11之间,所以取12;
const int N = 12010, M = 2010;
int v[N], w[N];
int f[M];
int n,m;

int main(){
    scanf("%d%d", &n, &m);
    int cnt = 0;
    for(int i = 1; i <= n; i ++){
        int a,b,s;
        scanf("%d%d%d", &a, &b, &s);
        // k is to use package s
        int k = 1;
        while( k <= s){
            cnt ++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            // s -= k must be above the k *= 2
            s -= k;
            k *= 2;
        }
        // 存在s没有枚举完的情况,比如上述的 200的例子中最后的73
        // 最后s个物品一块儿打包了
        if( s > 0){
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    // 打包后转化成01背包问题
    n = cnt;
    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 的背包。

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

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

输出最大价值。

输入格式

第一行有两个整数 N,V用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

  • 每组数据第一行有一个整数 S i S_i Si,表示第 i 个物品组的物品数量;
  • 每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j , w i j v_{ij},w_{ij} vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式

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

数据范围

0<N,V≤100
0 < S i ≤ 100 0<S_i≤100 0<Si100
0 < v i j , w i j ≤ 100 0<v_{ij},w_{ij}≤100 0<vij,wij100

输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
解答与思路:

同样是解决两个问题,状态表示和状态计算,但是与多重背包不同,多重背包是枚举一个物品拿多少个;而分组背包是枚举第i组物品选哪个,或者不选

  • 状态的表示:f[i][j]
    • 表示的集合意义: 表示从前i组里选物品,并且总体积不超过j的所有集合
    • 属性:max,表示价值的最大值
  • 状态的计算(集合划分):
    • 不选第i组中的物品,和选第i组中第k个物品。

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] ) f[i][j] = max(f[i-1][j] , f[i-1][j - v[i][k]] + w[i][k]) f[i][j]=max(f[i1][j],f[i1][jv[i][k]]+w[i][k])

二维空间最初的版本:

注意: 这里不能写成

for(int i = 1; i <= n; i ++){
    for(int j = 0; j <= m; j ++){
        for(int k = 1; k <= S[i]; k ++)
            if( j >= v[i][k])
                f[i][j] = max( f[i-1][j] , f[i-1][j - v[i][k]] + w[i][k]);
    }
}

因为如果f[i][j] = max( f[i-1][j] , f[i-1][ j - v[i][k] ] + w[i][k]]; 假设f[i-1][j] = 1, 且假设f[i-1][ j - v[i][k] ] + w [i][k]一共有三个值,分别是6,5,4; (由于S[i] = 3, 一共有三个物品); 当在最下面一层for循环里进行比较时,f[i][j] = max(1, 6); f[i][j] = max(1, 5) ; f[i][j] = max(1, 4) 最后一层循环过完,结果f[i][j] = 4; 而本应该是1,6,5,4中取最大值6.

#include<bits/stdc++.h>

using namespace std;

const int N = 110;
int w[N][N], v[N][N], S[N];
int f[N][N];
int n,m;
int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++){
        scanf("%d", &S[i]);
        for(int j = 1; j <= S[i]; j ++){
            scanf("%d%d", &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;
}

一维空间优化版:

#include<bits/stdc++.h>

using namespace std;

const int N = 110;
int n,m;
// w[i][j]来存储第i组内第j个物品的价值
// v[i][j]来存储第i组内第j个物品的体积
// s[i] 表示第i组里的个数
int w[N][N], v[N][N], s[N];
int f[N];

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++)
    // 背包容量,倒着枚举,因为会出现上一层的情况
        for(int j = m ; j >= 0; j--)
            for(int k = 0; k < s[i]; 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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值