Description
有n种硬币,面值分别为V1,V2,V3,…..Vn,每种都有无限多。
给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S?
输出硬币数目的最小值和最大值。1<=n<=100, 0<=S<=10000,1<=Vi<=S。
Input Data
输入有三行:第一行一个整数S;第二行一个整数n,表示硬币种数;第三行n个整数,表示硬币面值
Output Data
输出四行:前两行为最小值,和组成最小值的方案;后两行为最大值和方案。若有多种方案,输出字典序最小的(即序号尽量小的),方案中之只包含面值的序号。
Input / Output Sample
6
4
3 1 2 5
Output Sample
2
1 1
6
2 2 2 2 2 2
————————————————分割の线————————————————
分析
这是一道dp的入门好题。显然是一种无限背包的变形题(将最大和最小相融合),除此之外还要注意opt的记录。
对于背包不熟悉的,可以尝试一下[luoguP1060]开心的金明
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,s;
int v[200];//不同硬币的面值
int f[10010],opt[10010][2];
//f[i]表示距离目标面值差i的方案数
//opt[i][0]表示从转移到i点前原始状态,opt[i][1]表示转移时使用的硬币的序号
void write(int x)
{
if(opt[x][0]!=s) //如果当前状态不为初始状态时,再次通过opt进行回溯
{
printf("%d ",opt[x][1]);
write(opt[x][0]);
return ;
}
else printf("%d",opt[x][1]);//注意省略文末空格
return ;
}
int main()
{
cin>>s>>n;
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
memset(f,-1,sizeof(f));//将除初始状态之外的其他状态,全标记为未赋值(-1)
memset(opt,0,sizeof(opt));//其实是不需要的
f[s]=0;
for(int i=n;i>=1;i--)//从后往前枚举,注意取的是≥(>=),如此可以有效更新字典序比它大的原始序列。想一下这是为什么?
for(int j=s;j>=v[i];j--)//注意是无限背包问题
{
if(f[j]!=-1)
if(f[j-v[i]]==-1||f[j-v[i]]>=f[j]+1)//刷表法,从当前状态出发更新f[j-v[i]]的值
{
opt[j-v[i]][0]=j,opt[j-v[i]][1]=i;
f[j-v[i]]=f[j]+1;
}
}
printf("%d\n",f[0]);//输出方案数
write(0);//回溯输出使用硬币的序列
cout<<endl;
//如下同上
memset(f,-1,sizeof(f));
memset(opt,0,sizeof(opt));
f[s]=0;
for(int i=n;i>=1;i--)
for(int j=s;j>=v[i];j--)
{
if(f[j]!=-1)
if(f[j-v[i]]==-1||f[j-v[i]]<=f[j]+1)
{
opt[j-v[i]][0]=j,opt[j-v[i]][1]=i;
f[j-v[i]]=f[j]+1;
}
}
printf("%d\n",f[0]);
write(0);
cout<<endl;
return 0;
}