【 题目描述 】
金银岛上的人使用金币,每种金币面值分别是 A1; A2; A3; : : : ; An 元。一天 Tony 决定在
附近商店买一个非常好的表,他想在付钱的时候不要找零,但是他发现他的钱包里每种金
币的数量分别只有 C1; C2; C3; : : : ; Cn 个。不过,Tony 知道这块表的价格不会超过 M 元金币
(他不知道表的精确价格)。不知他的付钱方式能否实现。
你的任务是帮助 Tony 算一下,在 1::M 元范围内(包括边界),他钱包中的金币可以精
确支付多少种价格。
【 输入格式 】
输入包括多组测试数据。每组测试数据的格式如下:
第一行包括 2 个整数 n; M。
第二行包括 2n 个整数 A1; A2; A3; : : : ; An 和 C1; C2; C3; : : : ; Cn。
测试数据的最后一行有 2 个 0,这一行无需处理。
【 输出格式 】
每组测试数据输出一行,为一个整数,即能精确支付的价格种数。
【输入输出样例】
coin.in coin.out
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
8
4
【 数据规模与约定 】
对于 30% 的数据,1<=N<=30; 1<= M<=1000
对于 60% 的数据,1<=N<=60
对于 100% 的数据,1<=n<=100; 1<=M<=10^5;1<=Ai<=10^5;1<=Ci<=1000
————————————————分割の线——————————————————
楼天成男人八题入门级,如下提供四种解法:
Way1
本质:多重背包转完全背包
因为是有多个,所以可以先看作是完全背包,但在状态转移的过程中,使达f[i]状态时使用的当前种类的物品尽可能的少(即如果之前有达到过面值i,则不进行更行)
详细问题看代码:
#include
#include
#include
#include
using namespace std;
int f[100100],cnt;//f[i]表示面值为i能否达到
int g[100100];
int a[110],c[110];//记录单个面值和数量
int main()
{
freopen("coin.in","r",stdin);
freopen("coin.out","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
while(n!=0||m!=0)//如果n,m都等于0则跳出循环
{
cnt=0;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&c[i]);
memset(f,0,sizeof(f));//先都赋值为达不到
memset(g,0,sizeof(g));//先默认不需要使用
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
if(f[j]&&f[j+a[i]]==0&&j+a[i]<=m&&g[j]//f[j]->只有当前状态成立,转移的状态才会成立
{//f[j+a[i]]==0->减少更新,以使g[i]尽可能小,能到达更多的状态;
f[j+a[i]]=1;
g[j+a[i]]=g[j]+1;//此处可以用g[j][i]表示到状态j使用i物品最少个数,因为只与当前第i种物品有关,所以可以用滚动数组保存,以省一维空间。
}
for(int i=1;i<=m;i++)
if(f[i])cnt++;//如果面值可以达到则可能数++
cout<scanf("%d%d",&n,&m);
}
return 0;
}
方法一结束
Way2
本质:有可行数统计的完全背包
即是通过判断是否可行,并以滚动数组进行维护,与Way1有几分神似
详见代码:
#include
#include
#include
using namespace std;
int n,m,f[100100],c[110],v[110],ans=0;
//v[i]表示第i种面值,c[i]表示第i种数量,n、m不解释
void mpAble()
{
memset(f,-1,sizeof(f));//全部赋值为不可行
f[0]=0;//无论何时,面值为0一定可行
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
if(f[j]>=0) f[j]=c[i];//如果原来是可行的则还可以再放c[i]个硬币
else f[j]=-1;//如果原来是不可行的,那第i种面值也不可行
}
for(int j=0;j<=m-v[i];j++)
if(f[j]>0)
f[j+v[i]]=max(f[j+v[i]],f[j]-1);//如果可以放置,比较放置后的最大可行性。
}
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0) break;
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
for(int i=1;i<=n;i++)
scanf("%d",&c[i]);//如上完成读入
mpAble();//ans记录所有可行的面值数量
for(int i=1;i<=m;i++)
if(f[i]>=0) ans++;
cout<//输出
ans=0;
}
return 0;
}
方法二结束
Way3
本质:多重背包转01背包
针对至多为一千的物品数,可以划分为1,2,4,8……c[i]-1-2-4-8,通过二进制的进位划分成不同数量的集合
如此可以在不影响01背包正确性的情况下,把c[i]化为log2级,使得复杂度(n*m*log(c[i]))
#include
#include
#include
#include
using namespace std;
int n,m;
int a[105],c[105],wl[105],w[105][20];//w[i][j]表示第i种物品的第j种集合的物品数量
int f[100050];
void Init()
{//按照二进制划分背包数量
int j;
for(int i=1;i<=n;i++)
{
wl[i]=0;//w[i]表示划分个数
j=1;//j表示划分出的理想集合中i物品有j个
while(1)
{
if(j//如果剩余的物品数量可以被再次划分
{
c[i]-=j;//减去理想集合集合的数量
w[i][++wl[i]]=j;//记录理想集合中的物品数量数量(化理想为现实)
j=j<<1;//理想集合中物品数量进一位(乘2)
}
else//如果已经不能再被划分
{
w[i][++wl[i]]=c[i];//记录剩余的所有物品个数
break;//跳出循环,无需再做(理想破灭)
}
}
}
return ;
}
int main()
{
freopen("coin.in","r",stdin);
freopen("coin.out","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)return 0;//如果计算至文末,结束程序
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&c[i]);//如上为读入部分
Init();//划分背包数量的预处理
memset(f,0,sizeof(f));//默认所有的值均为不可能
f[0]=1;//面值为0,一定可行
for(int i=1;i<=n;i++)//枚举物品种类
for(int k=1;k<=wl[i];k++)//枚举数量的集合数量
for(int j=m-a[i]*w[i][k];j>=0;j--)//枚举面值
f[j+a[i]*w[i][k]]=f[j+a[i]*w[i][k]]|f[j];//如果f[i]==1,则f[j+a[i]*w[i][k]],用按位或进行无脑运算
int ans=0;
for(int i=1;i<=m;i++)
ans+=f[i];//统计可行的面值数
cout<return 0;
}
方法三结束
Way4
本质:多重背包
在不改变多重背包的性质下,用单调队列优化决策和运行速度。
有趣的是这道题的单调队列不需要删除队尾,所以可以定义FIFO队列,想一想这是为什么?
代码如下:
#include
#include
#include
#include
#include
using namespace std;
int n,m;
int a[110],c[110];
queue<int>q;//建立一个FIFO的队列q(此题的单调队列只进不出)
bool f[100100];
int main()
{
freopen("coin.in","r",stdin);
freopen("coin.out","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)return 0;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&c[i]);
memset(f,0,sizeof(f));
f[0]=1;
for (int i=1;i<=n;i++)//如下进行01背包,完全背包,多重背包的分解
{
int maxl=a[i]*c[i];
if (c[i]==1)//如果只有一件物品, 则进行01背包
{
for(int j=m;j>=a[i];j--)
if(f[j-a[i]])
f[j]=1;
}
else if(m<=maxl)//如果物品总面值大于最大面值,则进行完全背包
{
for(int j=a[i];j<=m;j++)
if (f[j-a[i]])
f[j]=1;
}//如下才是重头戏,完全背包的单调队列优化
else for(int j=0;j//避免重复
{
while(q.size()>0) q.pop();//清空队列
for(int k=j;k<=m;k+=a[i])//充分利用大小
{
while(q.size()>0&&k-q.front()>maxl) q.pop();//始末两点之间的距离应小于第i种物品的总价值
if(!f[k])//由此对于枚举c[i]和m有了较大的提升
{
if(q.size()>0)//只有队列不为空,即可以从某点到达
f[k]=1;
}
else q.push(k);//如果等于0,则入队尾
}
}
}
int ans=0;
for(int i=1;i<=m;i++)
if(f[i])ans++;//统计可行的面值数
printf("%d\n",ans);
}
return 0;
}
方法四结束
至此coins的四种方法全部整理完毕,如果有所遗漏请在下方评论区提出