背包问题

https://www.bilibili.com/video/BV1pY4y1J7na/?spm_id_from=333.788.comment.all.click&vd_source=6c2daed6731190bb7d70296d6b9746bb

【背包九讲专题】https://www.bilibili.com/video/BV1qt411Z7nE?vd_source=57dbd16b8c7c2ad258cccce5966c5be8

https://www.cnblogs.com/jbelial/articles/2116074.html

1. 01 背包问题

题目描述

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

逐个选择物品

  1. 不选,最优直接看前面空间i最大装多少
  2. 选,看选后剩余空间,用剩余空间看前一个最多装多少

判断1,2两个哪个多,每次选择都是当前容量下(选与不选i及其之前枚举元素)的局部最优

/*
    f[i][j] 表示只看前i个物品,总体积j的情况下,总价值最大多少
    
    result = max(f[n][0~V])

    // 状态转移函数
    f[i][j]:
        1. 不选第 i 个物品,f[i][j] = f[i-1][j];
        2. 选第 i 个物品,f[i][j] = f[i-1][j-v[i]] + w[i];

*/

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;

int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N][N]; // 动态规划数组

int main(){
    // 输入物品数量和背包容量
    cin >> n >> V;
    
    // 输入每个物品的体积和价值
    for(int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    
    // 初始化动态规划数组
    // 可以不初始化,因为默认是0,但为了清晰可以显式初始化
    memset(f, 0, sizeof(f));
    
    // 动态规划填表
    for(int i = 1; i <= n; i++){ // 遍历每个物品
        for(int j = 0; j <= V; j++){ // 遍历每个可能的体积
            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]); // 选第i个物品
            }
        }
    }
    
    // 寻找最大价值
    int result = 0;
    for(int j = 0; j <= V; j++){
        result = max(result, f[n][j]);
    }
    
    // 输出结果
    cout << result;
    
    return 0;
}

核心状态转移函数

// 枚举每个物品
for(int i=0;i< n;i++){
	// 枚举体积
	for(int j<m;j>=v[i];j--){
		f[j]=max[f[j],f[j-v[i]]+w[i]]
	}
}

复杂度 mn

dp【i】【j】 表示从前面【i-1】个物品里任意取,放到容量为j的背包里的最大值,这里的任意取其实是可以取1个 取2个 或者i-1个全取。

总之就是 每一个dp【i】【j】都是背包容量为j状态下的最优解。

递推公式怎么理解?
其实动态规划思想都是逐步推结果,每一步都是最优解。
当前物品可以选则拿或者不拿:
不拿:背包容量和背包里物品总价值都没有变化,和上一个状态下的总价值相同。
拿:背包容量减少,背包里的物品总价值=上一个状态下的总价值+当前物品的价值

考虑 i 个 物品的 v 容量下最优解 = max(考虑i-1个物品的最优解,第i个物品的价值 +考虑 i-1 个 物品的 剩余空间 容量下最优解)


优化到一维数组

为了防止数组更新覆盖,且剩余容量一定小于当前容量。可以从后向前更新i值

/*
    f[i][j] 表示只看前i个物品,总体积j的情况下,总价值最大多少
    
    result = max(f[n][0~V])

    // 逆推
    f[i][j]:
        1. 不选第 i 个物品,f[i][j] = f[i-1][j];
        2. 选第 i 个物品,f[i][j] = f[i-1][j-v[i]] + w[i];

*/

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;

int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N]; // 动态规划数组

int main(){
    // 输入物品数量和背包容量
    cin >> n >> V;
    
    // 输入每个物品的体积和价值
    for(int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    
    // 初始化动态规划数组
    // 可以不初始化,因为默认是0,但为了清晰可以显式初始化
    memset(f, 0, sizeof(f));
    
    // 动态规划填表
    for(int i = 1; i <= n; i++){ // 遍历每个物品
        for(int j = V; j >= v[i]; j--){ // 遍历每个可能的体积
            // 只更新不同的部分
            if(j >= v[i]){
                f[j] = max(f[j], f[j - v[i]] + w[i]); // 选第i个物品
            }
        }
    }
    
    // 输出结果
    cout << f[m];
    
    return 0;
}

2. 完全背包问题

题目描述

给定 n 种物品,每种物品有一个体积 v[i] 和一个价值 w[i]。你有一个容量为 V 的背包。每种物品可以选择无限次。求在不超过背包容量的情况下,能装入物品的最大总价值。

样例输入

3 5
1 2
2 4
3 4

样例输出

8
#include<bits/stdc++.h>

using namespace std;

const int N = 1010;

int n, V; // n: 物品数量, V: 背包容量
int v[N], w[N]; // v[i]: 第i个物品的体积, w[i]: 第i个物品的价值
int f[N]; // 动态规划数组,f[j]表示容量为j时的最大价值

int main(){
    // 输入物品数量和背包容量
    cin >> n >> V;
    
    // 输入每个物品的体积和价值
    for(int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    
    // 初始化动态规划数组
    memset(f, 0, sizeof(f));
    
    // 动态规划填表
    for(int i = 1; i <= n; i++){ // 遍历每个物品
        for(int j = v[i]; j <= V; j++){ // 遍历每个可能的体积,从小到大
            f[j] = max(f[j], f[j - v[i]] + w[i]); // 选择或不选择第i个物品
        }
    }
    
    // 输出结果
    cout << f[V];
    
    return 0;
}

为什么从小到大遍历

剩余容量小于j,含i的剩余容量的最优解先被考虑到。

考虑 i 个 物品的 v 容量下最优解 = max(考虑i-1个物品的最优解,第i个物品的价值 +考虑 i 个 物品的 剩余空间 下最优解)

数学归纳法:

  1. 考虑前i-1个物品之后,所有$f[j]$都是正确的
  2. 来证明,考虑完第i个物品后,所有$f[j]$也都是正确的

对于某个j而言,如果最优解中包含k个v[i]
$f[j]=max(f[j],f[j−k⋅v[i]]+k⋅w[i])$
则k-1的剩余容量的最优解会被先考虑到
$f[j]=max(f[j],f[j−(k-1)⋅v[i]]+(k-1)⋅w[i])$

3. 多重背包问题

对物品数量有限制

题目描述

你有一个容量为 V 的背包,以及 N 种物品。每种物品有如下属性:

  • 体积v[i]
  • 价值w[i]
  • 数量c[i]

每种物品可以选择的数量不超过 c[i] 个。你的目标是在不超过背包容量的情况下,选择物品使得总价值最大化。

3 7 // V N
1 1 3 // v w c
2 3 2
3 4 1

样例输出

9

基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取 n[i]件。注意需要j逆序,因为按照01背包考虑 取0件,取1件 是不同的状态,不能嵌套进自己的状态

可以两种初始转移状态

// 初始0状态所有容量的背包为0
1. f[i] = 0;
最大值 max = f[m]

// 初始0状态 容量为0的背包 为 0,其他容量的背包为-INF 不可达
2. f[0] = 0; f[i] = -INF, i!=0;
max(f[0...m])

第二种仅在剩余空间检查有恰好可以容纳的物品时转移,不允许有未使用的剩余空间,在向后遍历剩余空间有冗余时,不再转移更新,所以最大背包可能为-INF,需要向前查找

  1. 初始化方式一:所有 f[i] 初始化为 0,最终结果为 f[m]
// 初始化
for(int i = 0; i <= m; i++) {
    f[i] = 0;
}

// 动态规划填表
for(int i = 0; i < n; i++) { // 枚举每个物品
    for(int j = m; j >= v[i]; j--) { // 枚举背包容量
        for(int k = 1; k <= c[i] && k * v[i] <= j; k++) { // 枚举物品数量
            f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
        }
    }
}

// 最终答案
printf("%d", f[m]);
  1. 初始化方式二:f[0] = 0,其余 f[i] = -INF,最终结果为 max(f[0...m])
// 初始化
f[0] = 0;
for(int i = 1; i <= m; i++) {
    f[i] = -INF; // 表示不可达状态
}

// 动态规划填表
for(int i = 0; i < n; i++) { // 枚举每个物品
    for(int j = m; j >= v[i]; j--) { // 枚举背包容量
        for(int k = 1; k <= c[i] && k * v[i] <= j; k++) { // 枚举物品数量
            if(f[j - k * v[i]] != -INF) { // 确保状态可达
                f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
            }
        }
    }
}

// 最终答案
int max_val = -INF;
for(int i = 0; i <= m; i++) {
    max_val = max(max_val, f[i]);
}
printf("%d", max_val);

二进制优化

将 k 分为 01背包问题的几项,1、2、4

对于任意最终到达的k,可以转为是否选择 路径上 1 2 4...、【k-log(k)】 来表示

分割时最后一组不足以翻倍时可以直接加入

#include <stdio.h>
#include <stdlib.h>

#define MAX_V 20000  // 根据具体问题调整背包容量上限
#define MAX_N 100      // 根据具体问题调整物品数量上限

// 定义物品结构体
typedef struct {
    int v; // 体积
    int w; // 价值
    int c; // 数量
} Item;

// 函数声明
int max(int a, int b);

int main() {
    int V, N;
    Item items[MAX_N];
    int f[MAX_V + 1]; // 动态规划数组

    // 输入背包容量和物品数量
    scanf("%d %d", &V, &N);

    // 输入每个物品的体积、价值和数量
    for(int i = 0; i < N; i++) {
        scanf("%d %d %d", &items[i].v, &items[i].w, &items[i].c);
    }

    // 初始化DP数组
    for(int i = 0; i <= V; i++) {
        f[i] = 0;
    }

    // 动态规划填表,采用二进制优化
    for(int i = 0; i < N; i++) { // 枚举每个物品
        int v = items[i].v;
        int w = items[i].w;
        int c = items[i].c;

        // 二进制拆分
        for(int k = 1; c > 0; k <<= 1) { // k 每次翻倍,相当于 1, 2, 4, 8, ...
            int mul = k < c ? k : c; // 当前拆分的数量
            // 枚举背包容量,倒序遍历以防止重复计算
            for(int j = V; j >= mul * v; j--) {
                f[j] = max(f[j], f[j - mul * v] + mul * w);
            }
            c -= mul; // 更新剩余的数量
        }
    }

    // 输出最终答案
    printf("%d\n", f[V]);

    return 0;
}

// 辅助函数:取两个数的最大值
int max(int a, int b) {
    return (a > b) ? a : b;
}
posted @ 2025-03-10 19:25  丘狸尾  阅读(31)  评论(0)    收藏  举报