多重背包问题(详解二进制优化原理)

这篇具有很好参考价值的文章主要介绍了多重背包问题(详解二进制优化原理)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、问题描述

二进制多背包问题,# DP与贪心题目,算法,动态规划

二、思路分析

这道题同样是背包问题,那么它也同样满足三个性质:重叠子问题、最优子结构以及无后效性。那么这样的话,我们依然可以使用动态规划的思路去分析这道题目。那么动态规划的分析主要分为两步:状态转移方程的书写以及循环的设计。

1、状态转移方程

(1)状态表示:

我们在前面的两篇文章中介绍过,对于背包问题而言,我们一般用一个二维数组来表示dp数组,即我们经常写的: f ( i , j ) f(i,j) f(i,j)

那么 f ( i , j ) f(i,j) f(i,j)的意思是,当物品数量为 i i i,自己的背包容量是 j j j的时候,我们所能携带的最大价值: f [ i ] [ j ] f[i][j] f[i][j]

(2)状态转移:

状态转移的目的是为了能够将大规模的问题转化成较小规模的问题。

背包问题中,状态转移方程的书写就一个技巧:活在当下

我们此时共用 i i i个物品,我们不可能一下子就同时做出多个决策,从而得到最优解。我们就看眼前,我们眼前的就是第 i i i个物品,而我们要做的决策就是,第 i i i个物品到底选不选,选的话,在背包容量允许的条件下,我们选几个?

如果我们不选的话,我们就能写出下列的方程:

f ( i , j ) = f ( i − 1 , j ) f(i,j)=f(i-1,j) f(i,j)=f(i1,j)

由于我们不选第 i i i个物品,所以我们只需要考虑前 i − 1 i-1 i1个。这样我们就把规模从 i i i降低到了 i − 1 i-1 i1

假设我们选的话,那么就能够写成下列形式:( v [ i ] v[i] v[i]表示第 i i i个物品的体积, w [ i ] w[i] w[i]表示第 i i i个物品的价值)

f ( i , j ) = f ( i − 1 , j − v [ i ] ∗ k ) + w [ i ] ∗ k f(i,j)=f(i-1,j-v[i]*k)+w[i]*k f(i,j)=f(i1,jv[i]k)+w[i]k

上述等式成立的前提是保证: j ≥ v [ i ] ∗ k j\geq v[i]*k jv[i]k

我们只需要在上述的这些情况中取一个最大值即可:

所以我们的状态转移方程可以表示为:

f ( i , j ) = m a x { f ( i − 1 , j ) f ( i − 1 , j − v [ i ] ) + w [ i ] j ≥ v [ i ] f ( i − 1 , j − v [ i ] ∗ 2 ) + w [ i ] ∗ 2 j ≥ v [ i ] ∗ 2 . . . . . . f ( i − 1 , j − v [ i ] ∗ k ) + w [ i ] ∗ k j ≥ v [ i ] ∗ k f(i,j)=max \begin{cases} f(i-1,j)\\ f(i-1,j-v[i])+w[i] &j\geq v[i]\\ f(i-1,j-v[i]*2)+w[i]*2 &j\geq v[i]*2\\ ......\\ f(i-1,j-v[i]*k)+w[i]*k&j\geq v[i]*k \end{cases} f(i,j)=max f(i1,j)f(i1,jv[i])+w[i]f(i1,jv[i]2)+w[i]2......f(i1,jv[i]k)+w[i]kjv[i]jv[i]2jv[i]k

2、循环设计

我们的循环和之前所介绍的01背包问题、完全背包问题的循环设计是一样的,最外层循环是背包的规模从小到大,第二层的循环是背包的容量,从小到大。

三、代码模板

1、朴素版

我们综合上面的思路,就能够写出下面的代码:

#include<iostream>
using namespace std;
const int N=110;
int f[N][N],v[N],w[N],q[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d",v+i,w+i,q+i);
    }
    for(int i=1;i<=n;i++)//枚举物品规模
    {
        for(int j=0;j<=m;j++)//枚举背包容量
        {
            for(int k=0;k*v[i]<=j&&k<=q[i];k++)//书写状态转移方程
            {
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

2、优化版

我们发现上面的这个朴素版代码包含了三重循环,那么我们如何降低一层循环呢?

其实,我们的多重背包可以转化成01背包问题。这样讲肯定很抽象,大家可以看下面的图:
二进制多背包问题,# DP与贪心题目,算法,动态规划
但是这样做的话只是形式上做了优化,从 n 3 n^3 n3 n 2 n^2 n2,但实际上依旧是一个 n 3 n^3 n3的时间复杂度。

而这样没有成功优化的原因是因为我们有 n n n i i i物品,就需要重复 n n n个i物品。那么我们是否存在一种方式,我们不需要枚举 n n n次i物品,就能够表示 n n n i i i物品呢?

答案是有的。

我们的十进制数可以用二进制数来表示。

假设我们的二进制位有4位:

那么我们能表示的范围是: 0000 0000 0000 ~ 1111 1111 1111

1111 = 1000 + 0100 + 0010 + 0001 1111=1000+0100+0010+0001 1111=1000+0100+0010+0001

上面右侧的四个部分组成了这个数字。

我们可以从形式上掌握一下,这个四个部分所代表的含义就是对应的位数是1

1000 1000 1000:第四位是1
0100 0100 0100:第三位是1
0010 0010 0010:第二位是1
0001 0001 0001:第一位是1

接着我们发现, 0000 0000 0000 1111 1111 1111之间的任何一个数字,其实无非是某位是0,某位是1。如果某位是1,我们只需要加上上面四个数字中的其中一个。

例如:
1001 = 1000 + 0001 1001=1000+0001 1001=1000+0001
1101 = 1000 + 0001 + 0100 1101=1000+0001+0100 1101=1000+0001+0100

因此我们就能够得到一个结论,我们能够有这四个数字表示 0000 0000 0000 1111 1111 1111之间的所有数字。

转换成十进制而言,就是说我们能够用 1 1 1, 2 2 2, 4 4 4, 8 8 8表示出 0 0 0 15 15 15之间的任何数字。

所以,我们可以利用几个 2 k 2^k 2k,其中 ( k = 0 , 1 , 2 , . . . ) (k=0,1,2,...) k=012...,来表达一个范围内的所有数字。根据我们刚才的推导,所能表示的范围其实就是我们刚刚这几个数加在一起时的值,其实就是一个等比数列求和。

用数学公式表达就是

我们可以利用: 2 0 、 2 1 、 2 2 、 2 3 、 . . . 、 2 k 2^0、2^1、2^2、2^3、... 、2^k 20212223...2k表示 0 0 0 ~ 2(k+1) − 1 -1 1之间的所有数字。

那么如果我们的要表达的200

那么我们可以利用:
2 0 、 2 1 、 2 2 、 2 3 、 2 4 、 2 5 、 2 6 2^0、2^1、2^2、2^3、2^4、2^5、2^6 20212223242526

这几个数能表达的最大值是: 2 7 − 1 = 127 2^7-1=127 271=127

那么从128到200怎么表示呢?

其实我们只需要多加一个73即可。

也就是说,我们可以用:

2 0 、 2 1 、 2 2 、 2 3 、 2 4 、 2 5 、 2 6 、 73 2^0、2^1、2^2、2^3、2^4、2^5、2^6 、73 2021222324252673
来表达0-200。

为什么呢?

我们只用前面的这些 2 k 2^k 2k。那么我们能表示的是 0 − 127 0-127 0127
如果我们每次选择的时候都加上一个73,我们能表示的范围就是:
73 − 200 73-200 73200

虽然两部分之间有一点重叠的部分,但是重叠的话,无非就是我们重复计算了几个相同情况而已,并不影响我们结果的正确性。

那么我们发现,此时我们利用 O ( l o g n ) O(logn) O(logn)级别的数字表示了 O ( n ) O(n) O(n)

时间上做了非常大的优化。

而这种优化方式被称作二进制优化

如图所示:

二进制多背包问题,# DP与贪心题目,算法,动态规划
根据上面的思路,我们就能够写出下面的优化版本的代码:文章来源地址https://www.toymoban.com/news/detail-621491.html

#include<iostream>
using namespace std;
const int N=3e4+10;
int dp[N],v[N],w[N];
int n,m;
int main()
{
    cin>>n>>m;
    int cnt=1;
    for(int i=1;i<=n;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        int k=1;
        //进行 “打包” 转换:二进制优化,转换成01背包
        while(k<c)
        {
            v[cnt]=k*a,w[cnt++]=k*b;
            c-=k;
            k*=2;
        }
        if(c>0)
            v[cnt]=c*a,w[cnt++]=c*b;
    }
    //利用01背包中的空间优化模板求解。
    for(int i=1;i<=cnt;i++)
        for(int j=m;j>=v[i];j--)
                dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
    cout<<dp[m]<<endl;
    return 0;
}

到了这里,关于多重背包问题(详解二进制优化原理)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 二进制加密PHP Webshell原理及简单实现

    今天继续给大家介绍渗透测试相关知识,本文主要内容是二进制加密PHP Webshell原理及简单实现。 免责声明: 本文所介绍的内容仅做学习交流使用,严禁利用文中技术进行非法行为,否则造成一切严重后果自负! 再次强调:严禁对未授权设备进行渗透测试! 为了躲避WAF、杀软

    2023年04月09日
    浏览(34)
  • C#对象二进制序列化优化:位域技术实现极限压缩

    目录 1. 引言 2. 优化过程 2.1. 进程对象定义与初步分析 2.2. 排除Json序列化 2.3. 使用BinaryWriter进行二进制序列化 2.4. 数据类型调整 2.5. 再次数据类型调整与位域优化 3. 优化效果与总结 在操作系统中,进程信息对于系统监控和性能分析至关重要。假设我们需要开发一个监控程序

    2024年01月22日
    浏览(34)
  • C/C++ 高精度(加减乘除)算法二进制优化

    第一章 简单实现 第二章 压位优化 第三章 二进制优化(本章) 上一章《C/C++ 高精度(加减乘除)算法压位优化》实现了优化的高精度计算,采用int32的整型数组每个元素可以储存9个10进制数字,想要再进一步优化计算速度,可以改变数据存储方式,采用二进制存储数字。依然采

    2024年02月11日
    浏览(29)
  • [动态规划,二进制状态压缩] 旅行商问题

    题目描述 一个国家有 n 个城市,每两个城市之间都开设有航班,从城市 i 到城市 j 的航班价格为 cost[i, j] ,而且往、返航班的价格相同。 售货商要从一个城市出发,途径每个城市 1 次(且每个城市只能经过 1 次),最终返回出发地,而且他的交通工具只有航班,请求出他旅

    2024年01月24日
    浏览(33)
  • 多重背包问题——单调队列优化

    我们在之前的文章中曾经讲解过多重背包问题,当时我们讲解了两种方法,一种方法就是三重循环,这种方法最为朴素好想。但是这种方法的时间复杂度非常高,后来我们想到了二进制优化的方式。那么今天我们将再介绍一种更好的优化方式——单调队列优化。 在讲解这种优

    2024年02月15日
    浏览(27)
  • [ARM汇编]计算机原理与数制基础—1.1.2 二进制与十进制数制转换

    在计算机中,我们通常使用二进制数制来表示数据,因为计算机的基本电平只有两种状态:高电平(通常表示为 1)和低电平(通常表示为 0)。而在我们的日常生活中,我们习惯使用十进制数制。为了方便理解,我们需要掌握二进制与十进制之间的转换方法。 二进制转十进

    2024年02月08日
    浏览(38)
  • 三十八、动态规划——背包问题( 01 背包 + 完全背包 + 多重背包 + 分组背包 + 优化)

    0 1 背包问题: 条件:N 个物品容量为 V 的背包,每件物品最多用 1 次,其中物品信息体积为 Vi,价值为 Wi。 目标:选出物品,使价值最大(不一定装满背包)。 特点:每件物品 最多只用 1 次 完全背包问题: 特点:每一件物品都有 无限个 多重背包问题: 特点:每个物品

    2024年02月07日
    浏览(37)
  • [ARM汇编]计算机原理与数制基础—1.1.3 二进制补码

    在计算机中,为了表示有符号整数(即正数和负数),通常采用二进制补码表示法。二进制补码不仅可以表示负数,还能简化计算机的加法和减法运算。接下来,我们将介绍二进制补码的概念及其计算方法。 原码、反码和补码 在讨论补码之前,我们先了解一下原码和反码的

    2024年02月08日
    浏览(35)
  • C# 流Stream详解(1)——读写txt和二进制文件

    【读写txt文件】 电脑手机上有各种各样的文件,例如视频文件、图片文件、文本文件,其中读写txt文件是最简单的,有多种方式, 使用StreamReader和StreamWriter 使用TextReader和TextWriter   使用FileStream 使用File类提供的静态方法 上面几种方法代码都很长,一般来说我们几乎不会使

    2024年02月06日
    浏览(33)
  • linux二进制文件分析三大工具详解(ldd、readelf、nm)

    测试代码源码、源码如下: 编译命令 ldd 是 Linux 下的一个命令,用于查看可执行文件或共享库文件的动态链接库依赖关系。通过 ldd 命令,你可以确定一个可执行文件或共享库文件所依赖的动态链接库(也就是它们在运行时需要加载的库文件)。 OPTIONS(可选) : ldd 命令支

    2024年02月06日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包