问题描述:
使用一个货币系统{1,2,5,10,...}产生18 单位面值的一些可能的方法是:18x1, 9x2, 8x2+2x1,3x5+2+1,等等其它。
写一个程序来计算有多少种方法用给定的货币系统来构造一定数量的面值。保证总数将会适合long long (C/C++) 和Int64 (Free Pascal)。
例如:使用{1,2,5}三种货币各无限,来组合出10元,一共有10种方式。
怎么思考?
用dp[i][j] 来表示使用前i种货币组合成j元的方式有多少种。
对于第i种货币,可以不选,问题转变为 使用前i-1 种货币组合j元有多少种。 dp[ i-1 ][ j ]
如果选的话,可以选1个、2个、...j/w[i]个,然后用剩下的i-1种组合。 sum{ dp[ i-1 ][ j - k*w[i] ] } k = 1,2,3,...j/w[i]。
综合上面两种方式, dp[i][j] = sum { dp[ i-1 ][ j - k*w[i] ] } k = 0,1,2,... j/w[i]
笨方法:
dp[0][0] = 1; for(i = 1;i <= n;i++) { dp[i][0] = 1; for( j = 1 ; j<= sum; j++)//for( j = money[i-1] ; j<= sum; j++) { for(k = 0;k <= j/money[i-1];k++) { dp[i][j] += dp[i-1][j-k*money[i-1]]; } } }
0 1 2 3 4 5 6 7 8 9 10 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 2 2 3 3 4 4 5 5 6 5 1 1 2 2 3 4 5 6 7 8 10
观察,第一行,只使用第一个元素1时,所有的sum都只有一种解,就是i个1。
第二行,当有2的时候,当sum的值开始比2大的时候,除了只用1的解法外,还可以先放1个2,然后剩下的用1和2来填充。完全背包问题???
发现自己在算法细节上还有问题。
具体就是代码中的注释部分。
在二维实现方式中,j的下界是不能优化的。因为j<w[i-1]的时候,dp[i][j] = dp[i-1][j],应该复制传递下来。而没有传递的话就是0 了。
只有一维数组才能优化下界。因为都是用dp[j]表示,不赋值就是保存下来了。
一维实现:
第i次循环后,考虑用dp[j]表示用前i种货币,组合成j元的方式有多少种。
对于第i种货币,有两种考虑方式:
1、不用它。那么问题转变为,用前i-1种货币组合成j元。即 dp[i-1][j]也就是第i次循环之前的dp[j]。
2、用它。用1个、2个、...k个。
考虑:
既然肯定要用了,可以先选一个货币i占位。然后还需要组合出j-w[i]元来。有i种货币可选。即 dp[i][j-w[i]]。
绕不过去的一个坎:
担心这种计算方式,得到的组合种类有重复。证明下没有重复:
首先dp[i][j]表示的就是用i种组合出j的无重复方式个数。
1、第i个不用,dp[i-1][j]表示不用i的方式数。
2、第i个用了,先选一个占位,剩下dp[i][j-w[i]]表示使用0/1/2/3...k个货币i,加上前i-1,组合成j-w[i]的不重复种类个数。
再加上占位的这个,第二种方式代表的就是使用 1 2 3 ..k+1个货币i,组合成j的不重复可能数。
所以,没有重复。 0 和 使用的都包含了。
学DP感觉得一直自言自语,说着别人听起来感觉很白痴的话说服自己。。。
上代码吧。
注意边界条件:dp[0] = 1.因为不论有多少种货币,只要不取就是0.这是一种合法的组合方式。
int main() //货币系统 { int i,j,k,n = 3; int sum = 10; int w[3] = {1,2,5}; int dp[11]; memset(dp , 0 ,sizeof(dp)); dp[0] = 1; for(i = 0;i < n;i++) { for( j = w[i] ; j<= sum; j++) { dp[j] += dp[j-w[i]]; } } return 0; }