Featured image of post dp(三)——背包问题

dp(三)——背包问题

背包问题我感觉可能好做一点,对于中等难度的题目可能只要判断好背包类型,可能再加上一点优化或 其他算法就差不多能解决了。

这里的题目可能各位尝试感觉会说没多难,但是我还是感觉做题目比较难的是判断这是什么题目,要用什么方法解决,要不要优化。这里简单说一下如何判断背包问题。其实说起来很简单,对于一个在限制量的要求下去求最值的问题大多是可以用背包来解决的。

这里总结一下几种背包类型和对应的题目,但是下面列出的大多数可能难度在绿题的样子或者低一级,各位熟悉后可以再去尝试更难的锻炼自己应用能力。

01背包

简介

n个物品,m的容量,每个物品有对应的重量和价值,每个物品只能拿一个,问能得到的最大价值为多少

分析

这个也是dp思想,首先我们很清楚这个问题可以由最优子问题解决,这个就不多说了,我们这里就说下一般的两种做法。

二维DP矩阵说明

f[i][j]表示只考虑前i个物品,当容量为j时的最值

1. 矩阵基本信息

  • 行代表:物品(从0到n)
  • 列代表:容量(从0到m)
  • 单元格含义:f[i][j] 表示考虑前i个物品、容量为j时的最大价值

2. 矩阵结构示例

i\j(物品\容量) 0 1 2 m
0(无物品) 0 0 0 0
1 计算值 计算值 计算值 计算值
2 计算值 计算值 计算值 计算值
n 结果 结果 结果 最终结果

一维DP数组说明

其实就是对二维dp的空间优化,用f[i]表示在容量为i时能达到的最值

1. 数组基本信息

  • 索引代表:容量(从0到m)
  • 元素含义:f[j] 表示容量为j时能获得的最大价值(通过迭代更新实现空间优化)

2. 数组迭代过程示例

状态阶段 容量0 容量1 容量2 容量m
初始状态 0 0 0 0
处理第1个物品后 0 更新值1 更新值2 更新值m
处理第2个物品后 0 再更新值1 再更新值2 再更新值m
处理完所有物品 0 最终结果1 最终结果2 最终结果m

3. 核心特点

  • 仅用一个一维数组存储中间结果,空间复杂度从O(n×m)优化为O(m)
  • 迭代方向需从右到左(容量从m到0),避免单个物品被重复选择
  • 每次迭代对应一个物品的处理,通过覆盖旧值实现状态更新

01背包例题——搬砖

题目描述

这天,小明在搬砖。

他一共有 $n$ 块砖,他发现第 $i$ 砖的重量为 $w_{i}$,价值为 $v_{i}$。他突然想从这些砖中选一些出来从下到上堆成一座塔,并且对于塔中的每一块砖来说,它上面所有砖的重量和不能超过它自身的价值。

他想知道这样堆成的塔的总价值(即塔中所有砖块的价值和)最大是多少。

输入格式

输入共 $n+1$ 行, 第一行为一个正整数 $n$, 表示砖块的数量。

后面 $n$ 行, 每行两个正整数 $w_{i}, v_{i}$ 分别表示每块砖的重量和价值。

输出格式

一行,一个整数表示答案。

输入输出样例 #1

输入 #1
1
2
3
4
5
6
5
4 4
1 1
5 2
5 5
4 3
输出 #1
1
10

说明/提示

【样例说明】

选择第 $1$、$2$、$4$ 块砖,从上到下按照 $2$、$1$、$4$ 的顺序堆成一座塔,总价值为 $4+1+5=10$。

【评测用例规模与约定】

对于 $20 %$ 的数据,保证 $n \leq 10$;

对于 $100 %$ 的数据,保证 $n \leq 1000 ; w_{i} \leq 20 ; v_{i} \leq 20000$ 。

蓝桥杯 2022 国赛 B 组 J 题。

思路

这道题比正常的01背包难一点,因为他让我们不能无脑遍历,为什么呢。因为每块转的承重能力不一样我们如果强行遍历会错失最优解,所以我们的首要任务就是想办法找出最好的顺序(各位正常解题要判断是否要用dp,用什么类型,这个分析就先省略了,可以看前面的博客或者自己多做题后会有感觉的)。

为了找到这个排序,我们可以先减少数量来看,我们就思考两块砖i和j。我们想是不是存在一种可能如果i可以在j下面那么j也一定可以呢,这样我们排序后就不会错过最优解了,因为i能做的我j也能做(好热血啊)。那我们就试着推导一下。

当i能在下面承担是会满足两个条件(我们假设二者中间的总重为m1,上面的总重为m2)
·v_i >= m1 + m2 + m_j
·v_j >= m2
而要j能在下面承担是也要满足两个条件
·v_j >= m1 + m2 + m_i
·v_i >= m2
现在我们要用第一组条件推导出第二组,很明显第二组的第二个条件是满足的。我们就来证明第一个条件
观察到m1 + m2的部分是相同的,我们通过移项发现如果 v_j - m_i >= v_i - m_j 则满足,即v_j+m_j >= v_i+m_i。至此我们就得到了排序方案

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <bits/stdc++.h>
using namespace std;

#define endl '\n'
#define pii pair<int,int>
#define int long long

const int mod = 1e9+7;
const int inf = 0x3f3f3f;

int n;
const int N = 20005;
const int M = 1005;
int f[N];

struct node{
    int w, v;
    friend bool operator < (const node &a, const node &b){
        return a.w + a.v < b.w + b.v;
    }
}a[M];

void solve(){
    cin >> n;
    for(int i = 1; i <= n; i++){
        cin >> a[i].w >> a[i].v;
    }
    sort(a+1,a+1+n);
    int ans = -1;
    for(int i = 1; i <= n; i++){
        for(int j = a[i].w+a[i].v; j >= a[i].w; j--){
            f[j] = max(f[j], f[j-a[i].w]+a[i].v);
            ans = ans > f[j] ? ans : f[j];
        }
    }
    cout << ans << endl;
}

signed main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int t = 1;
    // cin >> t;
    while(t--){
        solve();
    }
    return 0;
}

完全背包

简介

一样的问题,但是完全背包的物品可以无限拿,处理方案和01背包相似,但是01背包是倒序背包容量避免一个物品拿多次,完全背包就正序就好了。

完全背包例题——纪念品

题目描述

小伟突然获得一种超能力,他知道未来 $T$ 天 $N$ 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。

每天,小伟可以进行以下两种交易无限次

  1. 任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;
  2. 卖出持有的任意一个纪念品,以当日价格换回金币。

每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。

$T$ 天之后,小伟的超能力消失。因此他一定会在第 $T$ 天卖出所有纪念品换回金币。

小伟现在有 $M$ 枚金币,他想要在超能力消失后拥有尽可能多的金币。

输入格式

第一行包含三个正整数 $T, N, M$,相邻两数之间以一个空格分开,分别代表未来天数 $T$,纪念品数量 $N$,小伟现在拥有的金币数量 $M$。

接下来 $T$ 行,每行包含 $N$ 个正整数,相邻两数之间以一个空格分隔。第 $i$ 行的 $N$ 个正整数分别为 $P_{i,1},P_{i,2},\dots,P_{i,N}$,其中 $P_{i,j}$ 表示第 $i$ 天第 $j$ 种纪念品的价格。

输出格式

输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。

输入输出样例 #1

输入 #1
1
2
3
4
5
6
7
6 1 100
50
20
25
20
25
50
输出 #1
1
305

输入输出样例 #2

输入 #2
1
2
3
4
3 3 100
10 20 15
15 17 13
15 25 16
输出 #2
1
217

说明/提示

样例 1 说明

最佳策略是:

第二天花光所有 $100$ 枚金币买入 $5$ 个纪念品 $1$;

第三天卖出 $5$ 个纪念品 $1$,获得金币 $125$ 枚;

第四天买入 $6$ 个纪念品 $1$,剩余 $5$ 枚金币;

第六天必须卖出所有纪念品换回 $300$ 枚金币,第四天剩余 $5$ 枚金币,共 $305$ 枚金币。

超能力消失后,小伟最多拥有 $305$ 枚金币。

样例 2 说明

最佳策略是:

第一天花光所有金币买入 $10$ 个纪念品 $1$;

第二天卖出全部纪念品 $1$ 得到 $150$ 枚金币并买入 $8$ 个纪念品 $2$ 和 $1$ 个纪念品 $3$,剩余 $1$ 枚金币;

第三天必须卖出所有纪念品换回 $216$ 枚金币,第二天剩余 $1$ 枚金币,共 $217$ 枚金币。

超能力消失后,小伟最多拥有 $217$ 枚金币。

数据规模与约定

对于 $10%$ 的数据,$T = 1$。

对于 $30%$ 的数据,$T \leq 4, N \leq 4, M \leq 100$,所有价格 $10 \leq P_{i,j} \leq 100$。

另有 $15%$ 的数据,$T \leq 100, N = 1$。

另有 $15%$ 的数据,$T = 2, N \leq 100$。

对于 $100%$ 的数据,$T \leq 100, N \leq 100, M \leq 10^3$,所有价格 $1 \leq P_{i,j} \leq 10^4$,数据保证任意时刻,小伟手上的金币数不可能超过 $10^4$。

思路

这个也不是板子题还是需要一些额外思考的,但是也不算太难因为也不需要额外的处理,属于一般题吧,毕竟除了学习的时候也不会做到板子题。

来分析思路。对于我们正常来想,我们要思考的就是每一天投入多少去购买能最大化收益。题目提到纪念品可以买完就卖所以我们可以不要考虑持有量全部转化成金币就好,其余部分就是正常完全背包。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <bits/stdc++.h>
using namespace std;

#define endl '\n'
#define pii pair<int,int>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
using i128 = __int128;

const int mod = 1e9+7;
const int inf = 0x3f3f3f;

int n,m,t;
const int N = 105;
const int M = 1e4+5;
int v[N][N];
int f[M];

void solve(){
    cin >> t >> n >> m;
    for(int i = 1; i <= t; i++){
        for(int j = 1; j <= n; j++){
            // 第i天第j个物品的价值
            cin >> v[i][j]; 
        }
    }
    int cur = m;
    for(int i = 1; i < t; i++){
        for(int c = 0; c <= cur; c++) f[c] = c;
        for(int j = 1; j <= n; j++){
            for(int k = v[i][j]; k <= cur; k++){
                f[k] = max(f[k], f[k-v[i][j]]+v[i+1][j]);
            }
        }
        cur = f[cur];
    }
    cout << cur;
}

signed main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int t = 1;
    // cin >> t;
    while(t--){
        solve();
    }
    return 0;
}

分组背包

简介

每一组只能挑一个物品,要求最值。理解起来也简单,就是01背包多了一个循环去跑一遍对应组里面的每一个物品

分组背包例题——通天之分组背包

题目背景

直达通天路·小 A 历险记第二篇

题目描述

自 $01$ 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 $01$ 背包,他的物品大致可分为 $k$ 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。

输入格式

两个数 $m,n$,表示一共有 $n$ 件物品,背包能承受的最大重量为 $m$。

接下来 $n$ 行,每行 $3$ 个数 $a_i,b_i,c_i$,表示物品的重量,利用价值,所属组数。

输出格式

一个数,最大的利用价值。

输入输出样例 #1

输入 #1
1
2
3
4
45 3
10 10 1
10 5 1
50 400 2
输出 #1
1
10

说明/提示

$0 \leq m \leq 1000$,$1 \leq n \leq 1000$,$1\leq k\leq 100$,$a_i, b_i, c_i$ 在 int 范围内。

思路

纯板子,就阐明一下这个板子的思路吧。我们输入的时候比如输入x,y,z对应重量价值和租号,用吧b[z]存z组有多少个,用f[z][b[z]]存这个物品的索引好去调用他们的重量和价值

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <bits/stdc++.h>
using namespace std;

#define endl '\n'
#define pii pair<int,int>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
using i128 = __int128;

const int mod = 1e9+7;
const int inf = 0x3f3f3f;

const int N = 1005;
const int K = 105;
int n,m;
int w[N], v[N], b[N],f[N][N],dp[N];

void solve(){
    cin >> m >> n;
    int cat;
    int t = 0;
    for(int i = 1; i <= n; i++){
        cin >> w[i] >> v[i] >> cat;
        t = t > cat ? t : cat;
        b[cat]++;
        f[cat][b[cat]] = i;
    }
    for(int i = 1; i <= t; i++){
        for(int j = m; j >= 0; j--){
            for(int k = 1; k <= b[i]; k++){
                if(j >= w[f[i][k]])
                    dp[j] = max(dp[j], dp[j-w[f[i][k]]]+v[f[i][k]]);
            }
        }
    }
    cout << dp[m];
}

signed main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int t = 1;
    // cin >> t;
    while(t--){
        solve();
    }
    return 0;
}

多重背包

简介

也是一个01背包变种,就是每个物品有确定的数量,去求最值,我们只需要把多个数量当成不同物品,那么又变成了01背包,但是可能复杂度高了点。我们这里就分享怎么通过二进制优化来处理

我们对于有n个的物品,不用把他们存成n个不同物品,我们把他们存成 2^0 + 2^1 + … + 2^i i个物品,这样就从O(n)变成了O(logn)。

多重背包例题——板子题(后面写树上dp的时候那题用了分组背包,这里就用板子展示一下大致思想吧)

题目大意

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

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

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

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

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

求最大值

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int MAX_V = 2005;

struct Good {
    int v, w;
};

int main() {
    int N, V;
    cin >> N >> V;
    
    vector<Good> goods;
    
    // 二进制优化处理多重背包
    for (int i = 0; i < N; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        
        // 将s个物品拆分成1,2,4,...,2^k,剩余数 的组别
        for (int k = 1; k <= s; k *= 2) {
            s -= k;
            goods.push_back({v * k, w * k});
        }
        if (s > 0) {
            goods.push_back({v * s, w * s});
        }
    }
    
    // 01背包问题
    vector<int> dp(V + 1, 0);
    for (auto good : goods) {
        for (int j = V; j >= good.v; j--) {
            dp[j] = max(dp[j], dp[j - good.v] + good.w);
        }
    }
    
    cout << dp[V] << endl;
    return 0;
}
使用 Hugo 构建
主题 StackJimmy 设计