一、问题描述
物品无限的背包问题:有n种物品,每种均有无穷多个。第 i 种物品的体积为Vi,重量为Wi。选一些物品装到一个容量为 C 的背包中,求使得背包内物品总体积不超过C的前提下重量的最大值。1≤n≤100, 1≤Vi≤C≤10000, 1≤Wi≤1000000.
二、解题思路
我们可以先求体积恰好为 i 时的最大重量(设为d[i]),然后取d[i]中的最大值(i ≤ C)。与之前硬币问题,“面值恰好为S”就类似了。只不过加了新属性——重量,相当于把原来的无权图改成带权图,即把“+1”变成“+W[j]”。这样,问题就变成了求以C为起点、终点任意的,边权之和最大的路径。
三、代码实现
1、记忆化搜索
之前纠结这种方法的时间复杂度,先给结果:O(maxn * maxc)。因为计算dp(s)时,如果dp[i]中i是从0-->C,
则dp[i] = max(dp[i],dp[i - V[j]] + W[j]),dp[i - V[j]]已经计算出来且保存,相当于得到dp[i]没有花费时间。如果dp[i]中i是从C-->0,
每次计算的都被保存且只计算一次,有几次小的递归,也相当于没有花费时间。
1 #include<stdio.h> 2 #include<iostream> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 7 const int INF = 0x3f3f3f3f; 8 const int maxn = 100 + 10; 9 const int maxc = 10000 + 10; 10 int n,V[maxn],W[maxn],C; 11 int d[maxc]; //d[i]表示总体积恰好为i时的最大重量 12 13 int dp(int s) 14 { 15 int& ans = d[s]; 16 if (ans != -1) return ans; 17 ans = - INF; 18 for (int i = 0; i < n; i++) 19 { 20 if (s >= V[i]) ans = max(ans, dp(s - V[i]) + W[i]); 21 } 22 return ans; 23 } 24 void slove() 25 { 26 memset(d, -1, sizeof(d)); 27 d[0] = 0; 28 int res = -1; 29 for (int i = 0; i <= C; i++) 30 res = max(res, dp(i)); 31 printf("%d ", res); 32 } 33 34 int main() 35 { 36 while (scanf("%d",&n) == 1 && n) 37 { 38 scanf("%d", &C); 39 for (int i = 0; i < n; i++) 40 scanf("%d%d", &V[i], &W[i]); 41 42 slove(); 43 } 44 return 0; 45 }
2、递推式
这种写法时间复杂度十分显然,与记忆化搜索相同,都是O(maxn * maxc)。但必须注意循环的顺序,比如容量只能从0-->C,而不能反过来,前一种写法则没有循环的顺序要求。
1 void slove() 2 { 3 fill(d, d + n, -INF); 4 d[0] = 0; 5 int res = -1; 6 for (int i = 0; i <= C; i++) //容量的循环顺序只能是从小到大 7 { 8 for (int j = 0; j < n; j++) 9 { 10 if(i >= V[j]) d[i] = max(d[i], d[i - V[j]] + W[j]); 11 } 12 res = max(res, d[i]); 13 } 14 printf("%d ", res); 15 }
3、两者比较
在得到状态转移方程之后,还需要思考如何编写程序。尽管在很多情况下,记忆化搜索程序更直观、易懂,但在0-1背包中递推法更理想。因为已知状态转移方程后,递推法的难点是循环顺序,而有了“阶段”定义后,循环顺序变得十分显然。