背包九讲二、完全背包问题

前面讲了0、1背包的做法,相信很多人和我一样已经摸到了动态规划的大门稍微理解了动态规划的思想,现在我们要向前进,开始分析完全背包问题,这其中会用很多重要且有趣的思想。

题目描述:有N 件物品和一个容量为V 的背包,每件物品有无限个。放入第i 件物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。

可以看出,完全背包与0、1背包的唯一区别在于,完全背包的每件物品都有无限个选择,你可以选择0、1、2、3……个第i个物品,而0、1背包则仅有选与不选两种选择。我们不如还是按照0、1背包问题的第一种做法的思想,使用一个二维数组来保存每一步的数,F[i][v]表示将前i个物品放入空间为v的包中所能得到的最大价值。但是由于每个物品可以选则无数个,我们的状态转移方程需要由:F[i][v] = max{F[i-1][v], F[i-1][v-C[i]] + W[i]} 转变为 F[i][v] = max{F[i-1, v-k*C[i]] + k*W[i] | 0 <= kC[i] <= v}, 也就是说完全背包问题需要加上一个选择物品个数的循环,总的复杂度可以认为是 O(NV\Sigma \frac{V}{C[i]}),由此我们可以看出理解0、1背包的重要性,因为后面所有的背包问题的思想多多少少都是基于0、1背包问题。给出代码,加深理解。

	private static int maxValue1(int N, int V, int[] W, int[] C) {
		// 由于是完全背包问题 每个物品有无限个可以选择
		// 第一种方法
		// 创造一个二维数组
		int [][] F = new int[N+1][V+1];
		// 完全背包就是要在零一背包基础上 加一个循环选择个数
		for(int i = 0; i < N+1; i++) {
			F[i][0] = 0;
		}
		for(int j = 0; j < V+1; j++) {
			F[0][j] = 0;
		}
		for(int i = 1; i < N+1; i++) {
			for(int v = 1; v < V+1; v++) {
				int res = F[i-1][v-1];
				for(int k = 0; k*C[i-1] <= v; k++) {
					int tmp = F[i-1][v-k*C[i-1]] + k * W[i-1];
					if (tmp > res) {
						res = tmp;
					}
				}
				F[i][v] = res;
			}
		}
		
		return F[N][V];
	}

第二种做法:由于我们每件物品最多只能选择\left \lfloor V/C[i] \right \rfloor个,那我们就可以将每个物品复制这些份,然后转化为0、1背包问题来求解,这样每件物品也只有了选与不选两种选择。但是这样做实际并不高效,我们考虑一个问题:如何选用最少的数来将给定的数组合出来?

这就要用到二进制优化的思想,无论选多少件第i个物品,总可以表示成若干个2^{k}件物品的和,如7,可以用1、2、4组合出来,只需要决定选与不选,这样的话就可以转化为0、1背包问题,时间复杂度为O(NVlog\left \lfloor V/C[i]] \right \rfloor),这个优化力度还是比较大的。

	private static int maxValue2(int N, int V, int[] W, int[] C) {
		// 第二种方法采用二进制优化的方法
		// 将每个物品拆成多个物品的和从而使用零一背包的解法
		int [][] F = new int[N+1][V+1];
		for(int i = 0; i < N+1; i++) {
			F[i][0] = 0;
		}
		for(int j = 0; j < V+1; j++) {
			F[0][j] = 0;
		}
	
		
		for (int i = 1; i <= N; i++) {
			
			ArrayList list = new ArrayList();
			for(int k = 1; k*C[i-1] <= V; k*=2) {
				list.add(k);
			}
			
			if (V - list.get(list.size() - 1)*C[i-1] > 0) {
				list.add(V - list.get(list.size()-1)*C[i-1]);
			}
			System.out.println(list);
			for (int k : list) {
				for (int v = 1; v < k*C[i-1]; v++) {
					if (k==1) {
						F[i][v] = F[i-1][v];
					}
					else {
						F[i][v] = F[i][v];
					}
					
					
				}
				for (int v = k*C[i-1]; v <= V; v++) {
					
					if (k==1) {
						F[i][v] = Math.max(F[i-1][v], F[i-1][v - k*C[i-1]]+k*W[i-1]);
					}
					else {
						F[i][v] = Math.max(F[i][v], F[i][v - k*C[i-1]]+k*W[i-1]);
					}
					
				}
			}
			
		}
		
		return F[N][V];
	}

这里需要注意当k != 1时,我们不能再使用F[i-1][v]来做,因为前面已经被第i件物品更新过了,因此需要用F[i][v]来做,这就是为什么代码中0、1背包部分需要加判断。下面使用一维数组来解决完全背包问题也是考虑这个问题从而得到的。

方法三:采用一维数组来做,这就和0、1背包最后一个做法原理相同,只不过0、1背包问题中我们的第二个循环是由V遍历大C[i]的,这是因为避免F[v]已经被第i个物品更新过,因为我们每个物品仅有选与不选之分,而到了完全背包这里恰恰相反,我们就是要让第i个物品更新F[v]多次,因此我们第二个循环要从C[i]到V,代码如下,这个比较绕,可以思考一番。

	private static int maxValue3(int N, int V, int[] W, int[] C) {
		// 使用一维数组来做 降低了空间复杂度
		// 这里F[v] 代表v空间的背包所能达到的最大价值
		int[] F = new int[V+1];
		for (int i=0; i <= V; i++) {
			F[i] = 0;
		}
		for (int i=1; i <= N; i++) {
			for (int j=C[i-1]; j <= V; j++) {
				F[j] = Math.max(F[j], F[j-1]+W[i-1]);
			}
		}
		return F[V];
	}

完全背包的难度较0、1背包稍大,大家可以一边写代码,一边思考是怎么一回事,B站上也有很多大佬的讲解。

另外,代码是我自己学过之后编的,不是很能保证正确性,如有问题或更好的写法,请赐教

你可能感兴趣的:(背包九讲)