[week11]选做题11-1 东东与ATM——动态规划(背包问题)

题意

一家银行计划安装一台用于提取现金的机器。
机器能够按要求的现金量发送适当的账单。
机器使用正好N种不同的面额钞票,例如D_k,k = 1,2,…,N,并且对于每种面额D_k,机器都有n_k张钞票。
例如,
N = 3,
n_1 = 10,D_1 = 100,
n_2 = 4,D_2 = 50,
n_3 = 5,D_3 = 10
表示机器有10张面额为100的钞票、4张面额为50的钞票、5张面额为10的钞票。
东东在写一个 ATM 的程序,可根据具体金额请求机器交付现金。
注意,这个程序计算程序得出的最大现金少于或等于可以根据设备的可用票据供应有效交付的现金。

Input

程序输入来自标准输入。 输入中的每个数据集代表特定交易,其格式为:Cash N n1 D1 n2 D2 … nN DN其中0 <= Cash <= 100000是所请求的现金量,0 <= N <= 10是 纸币面额的数量,0 <= nk <= 1000是Dk面额的可用纸币的数量,1 <= Dk <= 1000,k = 1,N。 输入中的数字之间可以自由出现空格。 输入数据正确。

Output

对于每组数据,程序将在下一行中将结果打印到单独一行上的标准输出中。

输入样例

735 3 4 125 6 5 3 350
633 4 500 30 6 100 1 5 0 1
735 0
0 3 10 100 10 50 10 10

输出样例

735
630
0
0

提示

第一个数据集指定一笔交易,其中请求的现金金额为 735。 机器包含3种面额的纸币:4张钞票 125、6张钞票 5和3张钞票 350。 机器可以交付所需现金的确切金额。
在第二种情况下,机器的票据供应不能满足所要求的确切现金数量。 可以交付的最大现金为 630。 请注意,在机器中组合钞票以匹配交付的现金有多种可能性。

在第三种情况下,机器是空的,没有现金交付。 在第四种情况下,请求的现金金额为 0,因此机器不交付现金。


分析

这是一道多重背包问题,利用动态规划解决。


  • 动态规划 ——背包问题

背包问题这一类型的题目都建立在0-1背包问题的基础上。

  • 什么是0-1背包?

从N个质量为ni、价值为vi的物体中选出一些放到一个容量为M的背包中,使得选出物体总价值V最大。每个物体只能被选择一次

  • 如何解决0-1背包问题?

这个问题利用动态规划所切分的动态模型就是将容量M的背包切分为M个容量递增的背包。针对这M个背包依次进行0-1背包问题求解。对每个背包而言,尝试将每个物体放入到当前的背包中,确定在当前容量下的背包中所能放下的物体得到的最大价值。显然,遍历到容量为M的背包时,所得到的答案就是问题的答案。

在对容量为j的背包,尝试放入第i个物体时(wi为第i个物体的质量、vi为其价值):

此时有两个选择,放或是不放。

放——则容量为j的背包在放入第i个物体后所得到的最大价值为容量为j - wi的背包所得到的最大价值加上vi。

不放——则容量为j的背包在第i个物体尝试后的最大价值依然是在尝试第i-1个物体后的价值。也就是保持之前的不变。

总结状态方程如下:
在这里插入图片描述

💡0-1背包代码模版

  1. 普通版:
    在这里插入图片描述

  2. 优化版(滚动数组

关键在于对于每个容量的背包尝试第i个物体时,所需要的内容只有其相同容量背包下当前的最大值以及容量为j - wi的当前最大值。

所以可以考虑去掉第一维,只保留背包容量的维度,这样就只考虑每个背包的最大价值即可,而不需要考虑是在尝试了第几个物体之后的最大价值。

但是对于第i个物体,不能从容量为0扫描到总容量M。因为在这里没有区分每个背包容量对应最大价值的状态,也就代表着在容量为j - wi的背包中的最大价值里可能已经包含了当前对于容量为j的背包待选的第i个物体。

但是题目要求每个物体只能放入一次,所以这里不能顺序,而是应该逆序对背包容量进行遍历,这样保证每个物体在对容量为j的背包进行尝试时,更小的背包里一定是没有选择过它的。也就能保证每个物体只会被选择一次。
在这里插入图片描述

  • 其他背包问题

背包问题除0-1背包问题外,还包括:

1. 完全背包

和0-1背包问题的不同是:一个物体可被选择无限次

状态转移方程:在这里插入图片描述
💡完全背包优化版代码模版

和0-1背包的思想相同,但是改为了正序遍历各容量的背包。显然,这就满足了完全背包问题中每个物体可以被取无限次的要求。
在这里插入图片描述

2. 多重背包

和0-1背包问题的不同是:一个物体可被选择有限次

状态转移方程:
在这里插入图片描述
💡多重背包优化版代码模版

  • 二进制拆分

一个物体有ci个,也就代表该物体可被选择0~ci次。那么实际上可以看作分成了ci组,每组有一个该物体,然后对每组决定是否选择。

例如,第i个物体一共有7个,也就是可以分为7组,每组一件第i个物体。如果其中有三组都选择放入,那就代表着该物体被选择放入了三件。

这就是通过拆分为组,将该问题化做0-1背包问题的根本。

但是问题在于如果分为每组一个,那么组数太大,需要进行判断的次数太大。那么二进制拆分就是将组数减少的办法。

减少的办法讲起来有点复杂,但是其实思想不太难。关键就是把ci分为几个数,即代表几个组。使得这几个数相加等于ci,且这三个数任意组合相加可以得到1~ci所有数,这就模拟了选择0~ci个物品的所有情况。

但是,要注意的是,拆分所得的所有组在选择的时候,每个组只能最多被选择一次。这和0-1背包问题一致。

拆分的方法和十进制转二进制的思想很像。

举个🌰 —— 7 - >(111)

其中将二进制拆分所得的100、010、001就分别代表4、2、1,显然这三个数满足上述要求:

分为三个组,分别含有1、2、4件物品

0 ——全不选 —— 共选择0件该物品
1 ——选择件数为1的组 —— 共选择1件该物品
2 ——选择件数为2的组 —— 共选择2件该物品
1+2=3 ——选择件数为1和2的组 —— 共选择3件该物品
4 ——选择件数为4的组 —— 共选择4件该物品
1+4=5 ——选择件数为1和4的组 —— 共选择5件该物品
2+4=6 ——选择件数为2和4的组 —— 共选择6件该物品
4+2+1=7 —— 全选 —— 共选择7件该物品

这就是二进制拆分的含义。

问题在于有些数并不能像7一样通过它的二进制直接拆分,

如, 15->(1101),含有1000、0100、0001

根据二进制可以分为件数有8、4、1的三个组,但是显然这三个数无法组合出1~15之间的所有数字,如2、3等。因此需要进行补充,那么补充的一个组中的件数就等于ci与已拆分所得件数的差。

也就是,在这个例子还需要补充第四个组,其中包含15 - 8 - 4 - 1 = 2件该物品。可以验证一下,这一定是正确的。

所以,二进制拆分就是对每个物品的总件数进行拆分,在原来基础上将所有物品分为单位为组的0-1背包。在代码中,新生成的0-1背包中的物品价值和质量就为当前拆分出的组中的件数与该物品价值与质量的乘积。也就是每个单位组的价值和质量就是其中所含物品的总和。

⚠️二进制拆分代码模版
在这里插入图片描述

拆分结束后,直接对新生成的以组为单位的新物品进行0-1背包问题解决即可。

3. 分组背包

和0-1背包问题的不同是:所有物体被分为k组,每个物体只有一件,每组中只能选一个物体

状态转移方程:
在这里插入图片描述
💡分组背包代码模版

代码中 w[k][u] 和 v[k][u] 分别表示第 k 组的第 u 件物品的属性值。
多一层对组数的循环。
在这里插入图片描述

4. 超大背包问题

和0-1背包问题的不同是:使放入物体的单位价值之和不超过背包容量,且总价值最大

本问题并不是动态规划问题。

需要注意的是,单位价值的指的是价值vi/质量wi。

这个问题运用的是二分枚举的思想来解决的。

首先将所有物品分为两组,将两组中所有子集的总价值和总质量进行枚举。再以其中一组为基准,对另一组进行二分遍历,找单位价值的最大值。


  • 题目分析

题目所要求的就是从给出数量的各面额纸币中选出一些,使得其面额总和最接近目标金额。每个面额的纸币有大于等于1的有限张数。

显然这是一个多重背包问题。但是需要注意的是,在这个问题里并不存在每个物体的质量,而背包容量的性质实际就是物品的价值。

背包 = 待提取金额
物品= 纸币
物品价值 = 纸币面额
物品数量 = 每个面额纸币对应的数量


  • 解决思路

首先对每个面额纸币的张数进行二进制拆分,得到分组后的若干组纸币面额。再对该面额进行0-1背包问题解决。


问题

循环的起点和终点!


总结

  1. 动态规划理解起来还是需要一定时间的
  2. 所有的动态规划背包问题在解决的时候,根本都在于其与0-1背包问题的互化。

代码

//
//  main.cpp
//  lab5
//
//

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

int cash = 0,number = 0;

pair<int, int> money[12];
int ans[100001] = {0};
int vv[100000];

int main()
{
    ios::sync_with_stdio(false);
    
    
    
    while( cin>>cash )
    {
        cin>>number;
        
        for( int i = 1 ; i <= number ; i++ )
            cin>>money[i].first>>money[i].second;
        
//        for( int i = number ; i < 12 ; i++ )
//            money[i] = {0,0};
        
        if( cash == 0 || number == 0 )
            cout<<0<<endl;
        else
        {
            //二进制拆分
            int cnt = 0;
            
            for(int i = 1 ; i <= number ; i++ )
            {
                int t = money[i].first;
                
                for(int k = 1 ; k <= t ; k <<= 1 )  //利用二进制拆分为可表示的几组
                {
                    cnt++;
                    vv[cnt] = k * money[i].second;
                    t -= k;
                }
                
                if( t > 0 )         //若直接拆分二进制无法表示所有情况
                {
                    cnt++;
                    vv[cnt] = t * money[i].second;
                }
            }
            
            
            for( int i = 0 ; i <= cash ; i++ )
                ans[i] = 0;
            
            //这里的w和v都为面额,对拆分后的vv进行01背包
            //滚动数组
            for( int i = 1 ; i <= cnt ; i++ )
            {
                for( int j = cash ; j >= vv[i] ; j-- )
                {
                    ans[j] = max(ans[j],ans[ j - vv[i] ] + vv[i] );
                }
            }
            
            cout<<ans[cash]<<endl;
            
        }
        
        
    }
    
    
    
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天翊藉君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值