【第二十五课】动态规划:多重背包和分组背包(acwing-4,5,9 /思路 / c++代码)

目录

acwing-4

朴素代码 

acwing-5

二进制优化思路

代码

acwing-9

思路 

代码


在看多重背包和分组背包之前,要先有01背包和完全背包基础哦。指路👇

【第二十五课】动态规划:01背包问题(acwing-2 / 思路 / 含一维数组优化 / c++代码)

 【第二十五课】动态规划:完全背包问题(acwing-3 / 公式推导 / 思路理解 / 优化 / c++代码)

acwing-4

朴素做法(三层循环)比较好写,在之前的基础上,就不再多说了。只是要注意数据范围,稍微大点这个就用不了了。

朴素代码 

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int dp[N][N];
int v[N],w[N],s[N];
int main()
{
    cin>>n>>m;

    for(int i=1;i<=n;i++)cin>>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++)
            {
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<dp[n][m];
    return 0;
}

acwing-5

这里数据范围很大,用三层循环会超时 

二进制优化思路

相比完全背包来说,这里每件物品有多少件是题目给的,我们并不能确定。因此不能按照之前那样推导。

因此我们需要考虑其他的优化方法。

这里主要就是数据范围的问题。

我们要创建的v[i]w[i]数组分别表示能够存储所有可能的物品的价值和体积,N表示物品种数,V表示背包容量,每种物品的体积价值和数量最大都是2000,在这样的情况下,我们应该可以确定,创建的v[i]w[i]数组应该大小为1000*2000*2000,含义是最多1000种物品*每种物品最多有2000件*每件物品的最大体积/价值都是2000=4e9。这样开辟的数组空间简直是太大了。因此我们提出了二进制化的思想。

同样如果我们不对s进行处理,那么代码时间复杂度将会达到N*V*S=4e9

这里的二进制化是指对同一种有si件的物品进行二进制化

我们说n件不同的物品一共有2的n次方种选择方案,因为每种物品都有选与不选两种选择。但是对于si件相同的物品有多少种选择方案呢?其实只有(像完全背包那样的)0,1,2,3,...,si,种方案。如果我们要把这0-si全举出来,那情况就太多了。因此我们想到通过将相同数量的物品按照二进制的方式进行分组,可以减少要考虑的情况

也就是我们将这si件相同的物品按照二的次方来进行分组,比如7:分成1 2 4,这样只用1 2 4 就可以表示出0-7这八种可能,10:分成1 2 4 3 ,因为我们只要11种选择就可以了,最后一个数如果是2^3=8的话,1 2 4 8 所能组成的就不止11种了,因此我们最后一个数的选择一定是<8的。

通过这种分组方式,我们可以用每组中物品数量的组合来表示总数量,而不需要列举每种可能的组合。比如,对于si件物品,我们只需要考虑从每组中选取0到最大数量的可能性,而不需要列举每种具体的组合

如何理解这里0-7的八种情况可以被1 2 4这三个数凑出来?

我们分组后1 2 4 是把原来7个相同的物品,分成了3个不同的物品,这里所说的“不同的物品”,其实和题目所定义的不同种物品的含义是一样的,那么那些不同种物品怎么构成方案,这些重新组合而形成的不同种物品就怎么构成不同的方案。我们会将选1 2构成3,选2 4 构成6,其实不是2被用了很多次,只是在处理不同的组合方案。(我之前对这里有误解)

因此我们说二进制化之后,我们是把多重背包问题转换成了01背包问题


至此,思路我们应该讲明白了。

下面这个老师讲得太清晰了,非常推荐看一下~ 

 【多重背包的二进制优化-CSP/NOIP/蓝桥杯/ACM/青少年C++编程试听课】

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10, M = 2010;
int n, m;
int v[N], w[N];
int dp[M];
int main()
{
    cin >> n >> m;
    int count = 0;//记录当前物品种类的数量
    for (int i = 1; i <= n; i++)
    {
        int a, b, s;
        cin >> a >> b >> s;//因为可能要进行二进制化,所以不能直接复制到v w数组中
        int k = 1;//表示每种物品在二进制拆分时的数量,初始值为1,表示从拆分后的最小数量开始,也就是2的0次方
        while (k <= s)
        {
            count++;
            v[count] = a * k;
            w[count] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0)//处理剩余的物品数量,如果剩余数量不为0,则将剩余数量作为一个单独的物品添加到数组中。
        {
            count++;
            v[count] = a * s;
            w[count] = b * s;
        }
    }
    n = count;
    //直接写01背包的一维优化
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= v[i]; j--)
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

这样我们的时间复杂度就由N*V*S优化为N*logs*V

acwing-9

思路 

在分组背包问题中,对于每组物品,我们的选择是放入该组中的一个物品或不放入该组任何物品。这与01背包问题的情况类似,只是分组背包问题中的每组物品可以看作是01背包问题的一个子问题。

也就是说我们这一层的数据需要通过上一层才能得到。因此在遍历背包容量时,也应该逆序遍历。

从思路上来说:在分组背包问题中,每组物品内的物品之间是相互影响的,因此,为了确保状态的正确性,在考虑放入当前组的物品时,需要确保之前组的物品已经正确地放入背包,所以逆序遍历背包容量可以保证每次更新都是基于之前状态的正确值。

代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int dp[N];
int main()
{
    cin>>n>>m;//n表示组数

    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=m;j>=0;j--)
        {
            for(int k=0;k<s[i];k++)//涉及到不选这组物品/选这组物品的哪一个
            {
                if(v[i][k]<=j)
                {
                    dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
                }
            }
        }
    }
    cout<<dp[m];
    return 0;
}

在 01背包问题中,j >= v[i] 中的 i 表示的是当前物品的索引,而不是组的索引对于每个物品 i,我们都知道它的体积 v[i],所以可以直接在循环中检查背包容量是否大于等于当前物品的体积

在分组背包问题中,i 表示的是组的索引,每个组内的物品数量和体积是不确定的所以我们需要通过内部循环遍历每个组内的物品,并检查每个物品的体积是否小于等于当前的背包容量。因此不能像 01背包问题中那样直接使用 j >= v[i]。


那背包问题就写到这里啦。

有问题欢迎指出,一起加油!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值