- 较为简单的背包DP。设dp[i][j] = 1,到第i种钞票能形成cash j,否则为0. 则很容易得出转移方程,若dp[i][j] = 1,则dp[i+1][j+k*d[i+1]] = 1,其中k在[0,n[i+1]]之间,n[i+1]为第i中钞票数量,d[i]为其面额。若直接暴力计算复杂度为N*M*n。很容易想到若直接枚举i中的j与k个d[i+1]的和,有很多重复计算,例如若第i中面额产生了2,4的cash,若i+1的面额为2,则i中的cash加上d[i+1]又到了4。
- 参考楼天城八题中的硬币方案中的算法:
- 在更新由第i+1种货币产生的cash前对前i中产生的cash遍历并给增值数组 billRemain赋初值为billNum[i+1],表示从此cash可以向后增值billRemain次第i+1种面额。
- 再次遍历上次货币产生的cash,此时若此cash能增值,并且其增值到的cash 还没有在前i次中产生,就标记增值后的cash,并且将新cash能增值的数目变为此时cash增值数目减一。
- 为什么要限定增值后的cash没出现过,因为若可以增值到的cash已经可以增值,说明它已经由前i次货币产生,则它可以增值的数目显然比从现在的cash增值过去 要多,并且也避免了上面提到的重复标记。
- 内存优化:首先标记数组是不需要的,可以直接利用增值数组来作为cash是否存在的标记。因为若一个cash存在,则经过算法它的增值数组值肯定大于等于0。如果cash不存在,则会一直标记成-1.可以从它的变化看出来,首先经过赋值会大于等于0,又因为新产生cash时规定现在的cash能增值的数目必须大于等于1,所以其增值到的cash可增值数目就大于等于0.然后其实两次遍历可以写在一起,因为若在第i次cash更新后某cash存在,则增值数组下一次就等于billNum[i+1],所以可以根据无后效性,因为cash更新时会遍历所有存在的cash,可以当其增值数组值使用后(不会再使用)直接赋billNum[i+1]给它。注意边界条件,当i遍历到最后一种货币时,赋值时会越界,所以赋1给越界值。
#include<cstdio>
#include<cstdlib>
#define _min(x,y) ((x)>(y)?(y):(x))
#define maxCash 100002
#define maxDenoNum 12
struct node
{
int deno;
int billNum;
}d[maxDenoNum];
int cmp(const void* x,const void* y)
{
return ((node*)x)->deno*((node*)x)->billNum - ((node*)y)->deno*((node*)y)->billNum;
}
short billRemain[maxCash];
int main()
{
int i,j;
int avaCash,minCash;
int cash,denoNum;
while(scanf("%d%d",&cash,&denoNum) != EOF)
{
memset(billRemain,255,sizeof(short)*(cash+1));
avaCash = 0;
minCash = 0;
for(i = 1;i <= denoNum;i++)
scanf("%d%d",&d[i].billNum,&d[i].deno);
qsort(d+1,denoNum,sizeof(node),cmp);
d[denoNum+1].billNum = 1;
i = 1;
while(!d[i].billNum&&i++); //排除增值为0的情况
billRemain[0] = d[i].billNum;
for(;i <= denoNum;i++)
{
avaCash += d[i].deno*d[i].billNum;
minCash = _min(avaCash,cash);
for(j = 0;j <= minCash;j++)
if(billRemain[j] != -1)
{
if(billRemain[j]&&j+d[i].deno <= cash&&billRemain[j+d[i].deno] == -1)
billRemain[j+d[i].deno] = billRemain[j]-1;
billRemain[j] = d[i+1].billNum;
}
}
for(i = minCash;i >= 0;i--)
if(billRemain[i] != -1)
{
printf("%d\n",i);
break;
}
}
return 0;
}