当前位置: 首页 > article >正文

背包问题学习

背包问题是常见的动态规划dp的问题

下面用到的符号:

  • 常用n表示物品数, m表示背包容积
  • f[i][j]表示i件物品, j的背包容量的最大价值
  • w[i]表示第i件物品的价值, v[i] 表示第i件物品的容量
  • f[0][0~m] = 0, 所以n可以从1开始遍历
  • 一般是有两层嵌套循环 第一层遍历物品, 第二层遍历背包容量, 第三层视情况, 若完全背包or多重背包 需要遍历决策 判断 k*v[i] <= j (背包容量)

0/1背包

下图是0/1背包的分析:
0/1背包问题的分析

题目 (0/1背包)

在这里插入图片描述

输入输出

输入输出

核心代码

for(int i = 1; i <= n; i++)
    for(int j = 0; j <= m; j++){ // 不难发现 f[i][] 都是依赖于 f[i-1][] 的
        f[i][j] = f[i-1][j];
    	if(v[i]<=j) f[i][j] = max(f[i][j], f[i-1][j-v[i]]+w[i]);    
}

优化: 降维 二维 -> 一维

需要注意的是在遍历背包容量的时候, 需要逆序遍历背包容量, 因为物品只能取一个和不取两种状态, 而逆序能够保证这个。

完整代码如下:

#include<iostream>
#include<algorithm>

using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N]; // 物品体积 价值
int f[N]; 

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1; i<=n; i++) scanf("%d%d",&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]);
       }
    }
    printf("%d\n",f[m]);
    return 0;
}

完全背包

下图是完全背包的分析:

完全背包分析

题目(完全背包 物品可以无限拿, 只要背包容量还能放得下)
题目和输入/输出
题目输入和输出

朴素做法(三层循环)

#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++) scanf("%d%d",&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] = max(f[i][j], f[i-1][j-k*v[i]]+w[i]*k);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

优化:

(通过列举 f[i][j] 以及 f[i][j-v]两种情况, 发现有很多相同之处, 只相差个v) => 化简得

f[i]][j] = max(f[i-1][j], f[i-1][j-v]+w) 两种状态, 一个是取第i件物品, 另一个是不取第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]);
     }
 }

其实上述代码与0/1背包的代码有很多雷同的地方(都是两种状态, 要么取第i件, 要么不取), 只不过 0/1 背包是 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件, 后面是取第i件的方案)
这个状态转移方程的含义可以这样理解:

  1. 不取第 i 件物品:这种情况下,背包的最大价值保持不变,即为 f[i][j]。这实际上是上一个状态的值,因为我们没有添加新的物品。
  2. 取第 i 件物品:如果我们决定把第 i 件物品放入背包中,那么背包的剩余容量变为 j-v[i]。因此,当前的总价值是放入这件物品之前的总价值 f[i][j-v[i]] 加上这件物品的价值 w[i]

对于上面代码的转化:

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

继续优化:

2维压缩成1维: 这里因为完全背包问题, 物品可以取无数次, 所以第二层循环循环遍历背包容量时, 顺序遍历

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;

多重背包

与上面的背包问题不同的是每个物品数量有上限: 0~s[i]
题目:
题目输入输出

状态划分: 两种 -> 取第i件物品 / 不取第i件物品

朴素做法

状态方程:f[i][j] = max(f[i-1][j-k*v[i]]+w[i]*k, f[i][j]), k = 0, 1, 2... s[i]

初级版 多重背包

 #include<iostream>
 #include<algorithm>
 using namespace std;

 const int N = 110;
 int n, m;
 int s[N], v[N], w[N];
 int f[N][N]; // 1~N 个物品, 背包容量为N 所能达到的最大价值 (max)
 int main(){
     scanf("%d%d", &n,&m);
     for(int i = 1; i<=n; i++){ // 输入第i个物品的体积、价值和重量
         scanf("%d%d%d",&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-1][j], f[i][j-k*v[i]]+w[i]*k);
         }
     }
     printf("%d\n",f[n][m]);
     return 0;
 }

当数据量比较大的时候, 上面的代码会出现 TLE, 因此我们需要对上述代码进行优化
优化:

困难版 多重背包
我们可以尝试展开f, 尝试寻找规律

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

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

但是从上式找不到其中规律,所以下述使用二进制拆解第i个物品个数s[i] 去优化上述代码

2^n的方式进行拆解包-> 01背包问题 s->logs 组, 且每组只能选一次 (拆分)

 #include<iostream>
 #include<algorithm>
 using namespace std;
 
 const int N = 25000, M = 2010; // 物品数量N NlogS 1000*log2000, 背包体积 M
 int n, m;
 int v[N], w[N]; // 物体体积, 价值
 int f[N]; // dp数组, 这里使用一维数组, 因为物品数量进行重组了
 
 /*
     多重背包的优化 (二进制对物品数量进行拆分)
     对物品进行拆分, 这样就少了一层循环(第三层k)
 */
 
 int main(){
     cin>>n>>m;
     int cnt = 0;
     for(int i = 1; i <= n; i++){
         int a, b, s;
         cin>>a>>b>>s; // 输入 第i种物品的体积, 价值
         int k = 1;
         while(k <= s){ // 拆分次数 s[i]!
             cnt++;
             v[cnt] = a * k; // k个第i件物品体积 k个 第i件物品体积为a
             w[cnt] = b * k;  // 更新第i个物品的价值
             s -= k;
             k *= 2;
         }
         if(s > 0){ // 剩余的(s个物品)为一件
             cnt++;
             v[cnt] = a*s; 
             w[cnt] = b*s;
         }
     }
 
     n = cnt; // 更新物品数量
     // 后继转为 0/1 背包问题, 每个物品 要么拿一次, 要么不拿
     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;
 }

上述介绍了常见的三种背包问题的思路和基础代码模板以及相应的优化。希望各位大佬能够给予指导以及相应的意见, 后续时间也会取更新更多更丰富的算法内容, 谢谢大家的观看♥~


http://www.kler.cn/a/161107.html

相关文章:

  • 【前端】深入浅出的React.js详解
  • 障碍检测与避障控制 ROS2机器人
  • 服务器数据恢复—分区结构被破坏的reiserfs文件系统数据恢复案例
  • Redis五种数据类型剖析
  • 前端入门一之ES6--面向对象、够着函数和原型、继承、ES5新增方法、函数进阶、严格模式、高阶函数、闭包
  • C字符串 | 字符串处理函数 | 使用 | 原理 | 实现
  • 数据结构与算法编程题50
  • NC 比telnet 强大网络命令
  • 我有才打造知识付费小程序
  • simulink trigger模块使用——多种调用案例分析
  • 出海电商中的技术护航:Socks5代理与代理IP的应用
  • 第16关 革新云计算:如何利用弹性容器与托管K8S实现极速服务POD扩缩容
  • JVM垃圾回收机制GC
  • Spring cloud - gateway
  • 【微软技术栈】基于任务的异步编程
  • 简单3D姿态基线模型网络架构与验证【SIM】
  • 生成器简述 - python 基础进阶知识点补全(一)
  • 【C++】多线程(二):std::mutex std::atomic的使用
  • hive里如何高效生成唯一ID
  • 私域最全养号攻略---微信
  • springboot(ssm甘肃旅游工艺品商城 旅游管理系统Java(codeLW)
  • uniapp app将base64保存到相册,uniapp app将文件流保存到相册
  • Scrapy框架中间件(一篇文章齐全)
  • layui学习笔记
  • K8S pod无损上下线
  • Spark例子