题意:一些不同价值和一定数量的硬币,求用这些硬币可以组合成价值在[1 , m]之间的有多少。
分析:初始 d[] 为负无穷,然后多重背包,最后统计d[]中有多少是大于0的。
递归方程只需将完全背包题目的方程略微修改,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。有状态转移方程:
f[i][v] = max{ f[i-1][v-k*c[i]] + k*w[i] | 0 <= k <= n[i] }
复杂度是 O(V*Σn[i])。
当且仅当f[i]的状态只与f[i-1]有关时,可以将二维数组简化为一维。
解法一:
网友思路:多重背包。但不能直接转化为01背包求,因为数据太多。但是可以增加一个一维数组use[i],记录 到达i元时j种钱用的次数。
(dp[j - val[i]] && !dp[j] && use[j - val[i]] < num[i])
表示的是能由上一步得到,且当前硬币个数从未实现过,达到上一步时该种类硬币使用的最少个数+1不能超过该种类硬币总个数。每添加一种新的硬币,都要清零used数组,表示目前具备的任意状态都没有新硬币的参与。然后一种一种添加,直到超过该种硬币个数。
完整代码:
1 #include <stdio.h> 2 bool dp[100001]; 3 int use[100001]; 4 int n, m; 5 int val[101], num[101]; 6 int main(){ 7 dp[0] = 1; 8 while (~scanf("%d %d", &n, &m)){ 9 if (n == 0 && m == 0)break; 10 for (int i = 1; i <= n; ++i)scanf("%d", &val[i]); 11 for (int i = 1; i <= n; ++i)scanf("%d", &num[i]); 12 for (int i = 1; i <= m; ++i)dp[i] = 0; 13 int ans = 0; 14 15 for (int i = 1; i <= n; ++i){ 16 for (int j = 1; j <= m; ++j)use[j] = 0; 17 for (int j = val[i]; j <= m; ++j){ 18 if (dp[j - val[i]] && !dp[j] && use[j - val[i]] < num[i]){ 19 dp[j] = 1; 20 ++ans; 21 use[j] = use[j - val[i]] + 1; 22 } 23 } 24 } 25 printf("%d\n", ans); 26 } 27 return 0; 28 }
解法二:
定理:正整数n可以被分解成1, 2, 4,…, 2^(k-1), n-2^k+1 (k是满足n-2^k+1>0的最大整数)的形式,且1~n之内的所有整数均可唯一表示成1, 2, 4, …, 2^(k-1), n-2^k+1中某几个数的和的形式。
1、2、4可以组合出所有小于8的数;
1、2、4、8可以组合出所有小于16的数;
1、2、4、8、16可以组合出所有小于32的数;
将第i种物品分成若干件物品,每件物品有一个系数,这件物品的费用和价值均是本来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n[i] - 2^k + 1,且k是满足n[i] - 2^k + 1 > 0的最大整数。例如,n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出。
这样就将第i种物品分成了O(log n[i])种物品,将原题目转化为了错杂度为O(V*Σlog n[i])的01背包题目,是很大的改进。
完整代码:
#include <stdio.h> bool dp[100001]; int n, m; int val[101], num[101]; int main(){ dp[0] = 1; while (~scanf("%d %d", &n, &m)){ if (n == 0 && m == 0)break; for (int i = 1; i <= n; ++i)scanf("%d", &val[i]); for (int i = 1; i <= n; ++i)scanf("%d", &num[i]); for (int i = 1; i <= m; ++i)dp[i] = 0; int ans = 0; for (int i = 1; i <= n; ++i){ int cnt = num[i]; for (int k = 1; k <= cnt; k <<= 1){ for (int j = m; j >= val[i] * k; --j){ dp[j] |= dp[j - val[i] * k]; } cnt -= k; } if (!cnt)continue; for (int j = m; j >= val[i] * cnt; --j){ dp[j] |= dp[j - val[i] * cnt]; } } for (int i = 1; i <= m; ++i)ans += dp[i]; printf("%d\n", ans); } return 0; }