一、题意
1.简述
一家银行计划安装一台用于提取现金的机器。机器能够按要求的现金量发送适当的账单。机器使用正好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 的程序,可根据具体金额请求机器交付现金。
注意,这个程序计算程序得出的最大现金少于或等于可以根据设备的可用票据供应有效交付的现金。
2.输入格式
程序输入来自标准输入。 输入中的每个数据集代表特定交易,其格式为:Cash N n1 D1 n2 D2 … nN DN其中0 <= Cash <= 100000是所请求的现金量,0 <= N <= 10是 纸币面额的数量,0 <= nk <= 1000是Dk面额的可用纸币的数量,1 <= Dk <= 1000,k = 1,N。 输入中的数字之间可以自由出现空格。 输入数据正确。
3.输出格式
对于每组数据,程序将在下一行中将结果打印到单独一行上的标准输出中。
4.样例
Input
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
Output
735
630
0
0
Hint
第一个数据集指定一笔交易,其中请求的现金金额为 735。 机器包含3种面额的纸币:4张钞票 125、6张钞票 5和3张钞票 350。 机器可以交付所需现金的确切金额。
在第二种情况下,机器的票据供应不能满足所要求的确切现金数量。 可以交付的最大现金为 630。 请注意,在机器中组合钞票以匹配交付的现金有多种可能性。
在第三种情况下,机器是空的,没有现金交付。 在第四种情况下,请求的现金金额为 0,因此机器不交付现金。
二、算法
主要思路
该题就是一个多重背包问题。
多重背包问题可以转化为二进制拆分+01背包问题。01背包问题的状态转移方程和原理不再给出。下面具体解释二进制拆分如何降低时间复杂度。
什么是二进制拆分
考虑一个最简单的情况,假定
C
i
=
7
=
(
111
)
2
C_i = 7 = (111)_2
Ci=7=(111)2
那么可以将
7
7
7 简单地拆成
(
001
)
2
,
(
010
)
2
,
(
100
)
2
(001)_2, (010)_2, (100)_2
(001)2,(010)2,(100)2
即十进制的
1
×
v
i
,
2
×
v
i
,
4
×
v
i
1×v_i, 2×v_i, 4×v_i
1×vi,2×vi,4×vi
通过对这
3
3
3 组进行选、不选的 0-1 背包的决策,就能涵盖所有
0
C
i
0~C_i
0 Ci 中
的决策。从而转换成了 0-1 背包问题
7
=
(
111
)
2
7=(111)_2
7=(111)2 很好拆,但对于
13
=
(
1101
)
2
13=(1101)_2
13=(1101)2 这样的怎么处理?
简单地按照
(
1000
)
2
,
(
0100
)
2
,
(
0001
)
2
(1000)_2, (0100)_2, (0001)_2
(1000)2,(0100)2,(0001)2 这样处理?
显然不是!因为这样组合不能够凑出选
2
=
(
0010
)
2
2=(0010)_2
2=(0010)2 个这样的决策来。
二进制拆分
13
=
(
1101
)
2
13=(1101)_2
13=(1101)2,我们首先拆出
7
=
(
111
)
2
→
1
=
(
001
)
2
,
2
=
(
010
)
2
,
4
=
(
100
)
2
7=(111)_2 → 1=(001)_2, 2=(010)_2, 4=(100)_2
7=(111)2→1=(001)2,2=(010)2,4=(100)2
这样已经可以表示
0
到
7
0 到 7
0到7 范围内的所有数。剩下的不能表示的数共有
13
–
7
=
6
13–7=6
13–7=6 个,所以我们再将
13
13
13 拆出一个
6
6
6 来,似于偏移量的思路,通过控制
6
6
6 的选、不选,就能够表示所有的决策,也就是
[
0
,
7
]
+
0
×
6
或
[
0
,
7
]
+
1
×
6
[0,7]+ 0×6 或[0,7]+ 1×6
[0,7]+0×6或[0,7]+1×6 ,因此
13
=
(
1101
)
2
→
1
=
(
001
)
2
,
2
=
(
010
)
2
,
4
=
(
100
)
2
,
6
=
(
110
)
2
13=(1101)2 → 1=(001)_2, 2=(010)_2, 4=(100)_2, 6=(110)_2
13=(1101)2→1=(001)2,2=(010)2,4=(100)2,6=(110)2
注意:这里的理解与编码的思想有些许不同,可能同一个数字有不同的表示
方式,比如
6
6
6 可以表示为
2
=
(
010
)
2
+
4
=
(
100
)
2
2=(010)_2 + 4=(100)_2
2=(010)2+4=(100)2,也可以表示为
6
(
110
)
2
6(110)_2
6(110)2,但
这并不影响决策(该题我们只关心数量,而不是具体怎么选)。
复杂度降低到
O
(
N
×
V
×
l
o
g
T
)
O(N×V×logT)
O(N×V×logT)。
三、代码
#include<iostream>
#include<cstring>
#include<string>
#include<cstdlib>
using namespace std;
int cash,n,cnt=0;
int num[12];
int d[12]; //纸币的价值和体积相同
int dd[10000];
int f[100010];
void clr(){
memset(num,0,sizeof(num));
memset(d,0,sizeof(d));
memset(dd,0,sizeof(dd));
memset(f,0,sizeof(f));
}
void getdd(){
for(int i=1;i<=n;i++){
int t = num[i];
for(int k=1;k<=t;k<<=1){
dd[++cnt] = k*d[i];
t -= k;
}
if(t>0)
dd[++cnt] = t*d[i];
}
}
int main(){
while(scanf("%d",&cash)!=EOF){
clr();
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&num[i],&d[i]);
//二进制拆分
getdd();
//0-1背包问题求解
for(int i=1;i<=cnt;i++){
for(int j=cash;j>=0;j--){
if(j-dd[i]>=0)
f[j] = f[j]>(f[j-dd[i]]+dd[i])?f[j]:(f[j-dd[i]]+dd[i]);
}
}
int ans = f[cash];
printf("%d\n",ans);
}
return 0;
}