动态规划之背包问题(01背包问题、完全背包问题、多重背包问题 I、多重背包问题 II 、分组背包问题)_01背包问题时间复杂度为k-程序员宅基地

技术标签: 经验分享  算法  #动态规划  数据结构  算法与数据结构  

动态规划之背包问题

写在前面

之前讲过简单DP,经典01背包问题,在这我将会把背包问题更深入的讲解,希望能帮助大家更好的理解。

01背包问题

01背包问题二维到一维优化
先回忆一下这个图
在这里插入图片描述

在这我再将01背包问题代码发一遍,可以用来做对比。
在这里插入图片描述
二维:

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值 
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 

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 = 1; j <= m; j++)
        {
    
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

    cout << f[n][m] << endl;

    return 0;
}

一维:

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1005;
int f[MAXN];  // 

int main() 
{
    
    int n, m;   
    cin >> n >> m;

    for(int i = 1; i <= n; i++) {
    
        int v, w;
        cin >> v >> w;      // 边输入边处理
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }

    cout << f[m] << endl;

    return 0;
}

完全背包问题

在这里插入图片描述

完全背包问题和01背包问题的区别就在于完全背包问题中每件物品都有无限件可用。我们也可以先来试一下暴力写法。

#include<iostream>
using namespace std;

const int N = 1010;

int n, m;
int dp[N][N], 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 ++ )
            for(int k = 0; k * v[i] <= j; k ++ )//因为每一件物品都有无限件可用,我们只需要找出单件价值最高的商品就可以了
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
    cout << dp[n][m] << endl;
}

优化思路:

我们列举一下更新次序的内部关系:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w , …)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])

有了上面的关系,那么其实k循环可以不要了,核心代码优化成这样:

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]>=0)
        f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}

这个代码和01背包的非优化写法很像啊!!!我们对比一下,下面是01背包的核心代码

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

两个代码其实只有一句不同(注意下标)
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

因为和01背包代码很相像,我们很容易想到进一步优化。核心代码可以改成下面这样

for(int i = 1 ; i<=n ;i++)
    for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
    {
    
            f[j] = max(f[j],f[j-v[i]]+w[i]);
    }

综上所述,完全背包的最终写法如下:

#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[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;
}

多重背包问题 I

在这里插入图片描述
我们先来看这多重背包问题和01背包问题是不是很像,将s×v,s×w是不是就可以看成01背包问题了?

for(ll i=1;i<=n;i++)
    {
    
        cin>>a>>b>>c;
        for(ll j=1;j<=c;j++)
        {
    
            v[cnt]=a;
            w[cnt]=b;
            cnt++;
        }//将多重背包一个一个拆出来
    }

然后转换成01背包问题解决。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const ll N=1e5+100;

ll v[N],w[N];
ll f[N];
int main()
{
    
    ll n,m;
    ll cnt=1;
    cin>>n>>m;
    ll a,b,c;
    for(ll i=1;i<=n;i++)
    {
    
        cin>>a>>b>>c;
        for(ll j=1;j<=c;j++)
        {
    
            v[cnt]=a;
            w[cnt]=b;
            cnt++;
        }//将多重背包一个一个拆出来
    }
    for(ll i=1;i<=cnt;i++)
    {
    
        for(ll j=m;j>=v[i];j--)
        {
    
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }//01背包
    cout<<f[m];
    return 0;
}

多重背包问题 II

在这里插入图片描述
这道题和1看起来没什么区别,但是数据范围变了,数据范围变了如果不优化就话超时,那怎么优化呢?

我们只需要将转换成01背包问题那一部分优化了就可以了。

int cnt = 0;     // 将物品重新分组后的顺序
    for (int i = 1; i <= n; i ++)
    {
    
        int a, b, s;    // a 体积, b 价值, s 每种物品的个数
        scanf("%d %d %d", &a, &b, &s);

        int k = 1;   // 二进制拆分 打包时每组中有 k 个同种物品
        while (k <= s)  // 即y总说的: 最后一组的物品个数 < 2^(n+1)   1 2 4 8 16 ... 2^n 2^(n+1)
        {
    
            cnt ++;
            v[cnt] = a * k;  // 每组的体积
            w[cnt] = b * k;  // 每组的价值
            s -= k;
            k *= 2;  // 注意是 k * 2,每次增长一倍,不是k * k
        }

        if (s > 0)   // 二进制拆分完之后 剩下的物品个数分为新的一组
        {
    
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

为什么可以这样优化呢???

我们知道任何一个数都可以转化成二进制的数,那二进制和十进制的区别在哪呢?

一 、二进制与十进制

  1. 普通遍历问题
    遍历 n 个物品, 采用二进制计数方法遍历与采用十进制技术方法遍历的时间复杂度是一样的

    举例来说, 对于十进制数 8

    十进制遍历: 0,1,2,3,4,5,6,7 共 8 次遍历

    二进制遍历: 000, 001, 010, 011, 100, 101, 110, 111 共 8 次遍历

  2. 多重背包问题

    同样的道理, 对于多重背包问题, 采用二进制的遍历方法不能优化时间复杂度

    优化的原因在于引入了动态规划

二 、动态规划的时间复杂度估算

动态规划的时间复杂度 ≈≈ 问题的总个数 × 问题要做出的选择数

如, 对于 01 背包问题, 问题的总个数为N⋅V (N 为物品个数, V 为背包容量), 问题要做出的选择数为 2(选或不选)

则 01 背包问题的时间复杂度约为 2N⋅V

三 、多重背包
如果不采用动态规划的做法, 就像普通的遍历问题那样, 是否采用二进制的计数方法对时间复杂度的优化没有任何关系

但采用二进制的计数方法会影响问题的总个数与问题的选择数的乘积, 即动态规划做法下多重背包的时间复杂度

多重背包的动态规划时间复杂度

十进制遍历方法

问题的总个数为 N⋅V, N 为物品的种类数, V 为背包容量

问题的选择数约为 Smax,Smax 为每种物品数量的最大值

十进制下多重背包问题的 DP 时间复杂度为: N⋅V⋅Smax

二进制遍历方法
十进制下, 一种物品有 si个, 二进制下, 变为 1, 2, … , lgsi 个物品, 则共有 lgs1+lgs2+…+lg⁡sn 个物品, 约为 Nlgsmax 个物品

问题的总个数为 N⋅V⋅lgsmax

问题的选择数为 2

十进制下多重背包问题的 DP 时间复杂度为: 2N⋅V⋅lgsmax

最后请看代码

#include <bits/stdc++.h>

using namespace std;

const int N = 11 * 1000 + 10, M = 2010;

int v[N], w[N];
int f[M];

int main()
{
    
    int  n, m;
    scanf("%d %d", &n, &m);

    int cnt = 0;     // 将物品重新分组后的顺序
    for (int i = 1; i <= n; i ++)
    {
    
        int a, b, s;    // a 体积, b 价值, s 每种物品的个数
        scanf("%d %d %d", &a, &b, &s);

        int k = 1;   // 二进制拆分 打包时每组中有 k 个同种物品
        while (k <= s)  // 即y总说的: 最后一组的物品个数 < 2^(n+1)   1 2 4 8 16 ... 2^n 2^(n+1)
        {
    
            cnt ++;
            v[cnt] = a * k;  // 每组的体积
            w[cnt] = b * k;  // 每组的价值
            s -= k;
            k *= 2;  // 注意是 k * 2,每次增长一倍,不是k * k
        }

        if (s > 0)   // 二进制拆分完之后 剩下的物品个数分为新的一组
        {
    
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;  // 所有的组数即为 01背包中的物品个数

    // 写01背包模板
    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", f[m]);

    return 0;
}

分组背包问题

在这里插入图片描述
一、状态表示:f[i][j]

  1. 集合:从前i组物品中选,且总体积不超过j的所有方案的集合.
  2. 属性:最大值

二、状态计算:

  1. 思想-----集合的划分
  2. 集合划分依据:根据从第i组物品中选哪个物品进行划分.
    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);

请看代码

#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N][N];  //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N];   //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;

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=0;j<=m;j++){
    
            f[i][j]=f[i-1][j];  //不选 不选表示不选第 i 组物品的所有物品,只从前 i−1 组物品里面选
            for(int k=0;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;
}

因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积

#include<bits/stdc++.h>
using namespace std;

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

int main(){
    
    cin>>n>>m;
    for(int i=0;i<n;i++){
    
        cin>>s[i];
        for(int j=0;j<s[i];j++){
    
            cin>>v[i][j]>>w[i][j];
        }
    }

    for(int i=0;i<n;i++){
    
        for(int j=m;j>=0;j--){
    
            for(int k=0;k<s[i];k++){
        //for(int k=s[i];k>=1;k--)也可以
                if(j>=v[i][k])     f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);  
            }
        }
    }
    cout<<f[m]<<endl;
}

写到最后

最后看到这了,如果觉得自己有收获的话,可以给博主点个关注哦
觉得本篇文章不错的话记得收藏点赞,还有问题也可以评论留言
你的支持将是我继续创作的最大动力️️️
由于作者水平有限,如有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_54847231/article/details/123850995

智能推荐

Mac 储存空间“其他”占据这么多?一招带你搞定_其他用户与共享 mac 数据删除-程序员宅基地

文章浏览阅读3.5k次。手动清除这些“其他”文件的方法比较适合专业的、熟悉Mac规则的用户。对于普通Mac用户,如果怕误删系统文件,还是将清理“其他”文件的任务交给专业的第三方清理工具吧,例如CleanMyMac X。2、在“访达”中选中用户名,右侧空白处,右键“查看显示选项”-勾选“显示‘资源库’文件夹”1、打开“访达”-“偏好设置”-“边栏”中,勾选“个人用户名”;1、点击左上角  -“关于本机”-“储存空间”-“管理”;2、点击“文稿”-“文件浏览器”-“资源库”;1)安装应用程序,点击侧边栏中的“系统垃圾”。..._其他用户与共享 mac 数据删除

区块链赋能供应链金融风险管控探析_区块链对风险防控的作用-程序员宅基地

文章浏览阅读717次。本文转自:人民银行本溪市中心支行供应链金融是一种从产业链中来到产业链中去,不断夯实和优化产业链的生态金融,为有效破解中小微企业融资难开辟了一条新途径。但实践中,供应链金融风险管理一直面临难以突破的瓶颈,供应链金融风险无法准确评估和度量,履约风险无法有效控制,而且信息不对称大幅拉升风险管理成本,严重拖累了供应链金融的持续稳健发展,业务辐射范围受限。而区块链作为构建数字信任机制的全新模式,具有分布式记账、可追溯、不可篡改等技术特性,恰好直击供应链金融风控的痛点,为助推供应链金融稳健发展提供了切实可行的解._区块链对风险防控的作用

docker中mysql容器自动停止原因及解决方法_为什么我部署在docker里面的mysql,会被关闭-程序员宅基地

文章浏览阅读6.5k次,点赞3次,收藏23次。docker update -m 400M --memory-reservation 400M --memory-swap 500M 容器名。将docker.cnf 复制到mysql容器内(容器名可用docker ps -a 查看到name列,即为容器名)sudo docker cp ./docker.cnf 容器名:/etc/mysql/conf.d。第五步:限制mysql内存占用(本机器内存为1G,可视自己的机器内容设置)查看设置内容后docker容器内存使用情况:docker stats。_为什么我部署在docker里面的mysql,会被关闭

【K8S系列】深入解析K8S存储-程序员宅基地

文章浏览阅读2.3w次,点赞91次,收藏122次。在 Kubernetes 中,存储具有非常广泛的应用场景。可以根据实际需求选择适合自己的存储方案,以便更好地管理容器化应用程序中的数据和资源。本文会从以下三个方面,带你了解k8s存储:1.k8s存储类型;2.存储使用场景;3.存储使用案例

oracle 重新编译用户无效对象_oracle重新编译失效对象-程序员宅基地

文章浏览阅读1.2w次。oracle sys用户无效对象select owner,object_name, replace(object_type,' ','') object_type,to_char(created,'yyyy-mm-dd') as created,to_char(last_ddl_time,'yyyy-mm-dd') as last_ddl_time,statusfrom dba_o_oracle重新编译失效对象

【愚公系列】2023年10月 WPF控件专题 RadioButton控件详解_wpf radiobutton-程序员宅基地

文章浏览阅读5.1w次,点赞2次,收藏3次。WPF控件是Windows Presentation Foundation(WPF)中的基本用户界面元素。它们是可视化对象,可以用来创建各种用户界面。WPF控件可以分为两类:原生控件和自定义控件。原生控件是由Microsoft提供的内置控件,如Button、TextBox、Label、ComboBox等。这些控件都是WPF中常见的标准用户界面元素。自定义控件则允许开发人员使用XAML和C#等编程语言来创建个性化的用户界面元素。自定义控件可以根据需求提供更多的功能和自定义化选项,以及更好的用户体验。_wpf radiobutton

随便推点

初中学历学前端难不难_计算机前端初中学难不难-程序员宅基地

文章浏览阅读609次。初中学历学前端难不难那肯定难啊。如果年纪不大,而且对IT这方面又比较感兴趣,我建议先去想办法提升一下自己的学历,成人本科也是可以的,该说不说,这东西花点钱还是可以弄到的,毕竟现在IT行业还是很看重学历的,学历是工作的第一块敲门砖,可能有人会说能力更重要,但是我告你你没有学历,别人根本不会去了解你是否有能力。当然花钱买的那个学历也不是处处管用,像大一点的公司需要的学历是需要学信网认证的,但你搞个学历,很多你之前初中文凭进不去的公司可能就会考虑你。如果真的考虑好了的话我建议去报个短期的培训班,毕竟是初中文凭_计算机前端初中学难不难

ThreadX学习(2)——线程_threadx教程-程序员宅基地

文章浏览阅读3.9k次,点赞26次,收藏48次。ThreadX学习(2)——线程学习参考:ThreadX中的线程线程创建堆栈分配互斥锁线程优先级优先级反转优先级继承抢占阈值线程状态数据结构TCB就绪列表API学习参考:《Real-Time Embedded Multithreading: Using ThreadX and ARM》安富莱_STM32-V7开发板ThreadX内核教程(V0.7)ThreadX中的线程在ThreadX中,一般没有进程的概念,统称为线程。关于调度器的实现细节,ThreadX可能是用汇编写的,没看懂。T_threadx教程

苹果手机sim卡无效怎么办_苹果手机存储空间不足怎么办-程序员宅基地

文章浏览阅读127次。  虽然现在的手机存储空越来越大,不过,娱乐的文件已经各种软件的体积也越来越大,而不像其他安卓手机可以通过内存卡对存储空间进行扩充,苹果手机存储空间不足怎么办,下面就为大家介绍一下解决方法。苹果手机存储空间不足怎么办  步骤1:当手机存储空间不足时,我们先得要查清楚,究竟是什么占用了大量的手机存储空间。打开苹果主屏上的“设置”应用。  步骤2:在设置列表中找到“通用”项,点击进入,接下来在通用中找..._萍果7手机sm1卡失效怎么办

中国大学生计算机设计大赛省级赛事管理系统_计算机设计大赛管理信息系统类-程序员宅基地

文章浏览阅读1.1k次,点赞18次,收藏21次。根据提示输入参赛队编号,若查找成功,输出该赛事类别对应的基本信息(参赛作品名称、参赛学校、赛事类别、参赛者和指导老师信息),同时,输出查找成功时的平均查找长度ASL;能够管理各参赛队的基本信息(包含参赛队编号,参赛作品名称,参赛学校,赛事类别,参赛者,指导老师),赛事类别共11项(参见大赛官网。1、请根据任务描述的问题,设计合理的菜单,菜单交互设计要合理,便于用户根据提示使用系统的所有功能。来定义参赛队编号,参赛作品名称,参赛学校,赛事类别,参赛者,指导老师等基本信息。包括增加、删除、修改参赛队伍的信息。_计算机设计大赛管理信息系统类

2024软件测试学习线路图~-程序员宅基地

文章浏览阅读879次,点赞17次,收藏17次。一个好的心态和一个坚持的心很重要,很多冲着高薪的人想学习前端,但是能学到最后的没有几个,遇到困难就放弃了,这种人到处都是,就是因为有的东西难,所以他的回报才很大,我们评判一个前端开发者是什么水平,就是他解决问题的能力有多强。分享一些简单的前端面试题以及学习路线给大家,狂戳这里即可获取一个好的心态和一个坚持的心很重要,很多冲着高薪的人想学习前端,但是能学到最后的没有几个,遇到困难就放弃了,这种人到处都是,就是因为有的东西难,所以他的回报才很大,我们评判一个前端开发者是什么水平,就是他解决问题的能力有多强。

计算机毕业设计选什么题目好?springboot 高校宣讲会管理系统-程序员宅基地

文章浏览阅读128次。前方高能!享受这场视觉盛宴!计算机毕业设计选什么题目好?springboot 高校宣讲会管理系统

推荐文章

热门文章

相关标签