0/1背包问题(动态规划+动规优化)

本文介绍了动态规划解决0-1背包问题的方法,从二维数组的状态转移方程到一维数组的优化过程。通过动态规划求解在背包容量限制下能装入物品的最大价值,并探讨如何根据优化后的数组获取所选物品编号。优化后的算法在保持时间复杂度不变的情况下,空间复杂度降低到线性级别。
摘要由CSDN通过智能技术生成

题目地址

动态规划(二维数组)

大致描述就是有 N 个物品,每件物品有各自的重量 W 及价值 V,有一个最大容量为 C 的背包,求在不超过这个背包的容量下能装入物品的最大价值。(N,W,V,C都为整数)

假如对这些物品从1~i 进行编号,并用两个数组V、W分别保存物品的价值和重量,相应的 V[ i ] 和 W[ i ] 表示的意义是第 i 件物品的价值和重量。

首先定义F[ i ][ j ] = 在背包容量为 j 的情况下从前 i 件物品中能选取到的最大价值。这个定义的理解非常重要,它表示的是不同限制情况下的最优解。注意此处的 j 并不是题中描述的最大容量C,而是可以取从0~C的所有整数。

那么根据这个定义可以得到状态转移方程:F[ i ] [ j ] = max( F[ i-1 ] [ j ],F[ i-1 ] [ j-W[ i ] ] + V[ i ])

这个方程表示的意义是,在背包容量 j 的情况下对于物品 i :

  • 如果W[ i ]>j,物品重量超出背包容量肯定不能选,那么从前 i 个物品中装入等价于从前 i-1 个物品中装入,
    对应的状态转移方程 F[ i ] [ j ] = F[ i-1 ] [ j ] 。
  • 如果W[ i ]≤j,假设选入,注意这里是假设,那么有 F[ i ] [ j ] = F[ i-1 ] [ j-W[ i ] ] + V[ i ] ,也就是在背包总容量减去物品 i 重量后的基础上,前 i-1 件物品中的最优解+物品 i 的价值。

注意上面的 j 均表示的是背包总容量,而不是减去某物品重量后剩余的。

现在回到大方程 F[ i ] [ j ] = max( F[ i-1 ] [ j ],F[ i-1 ] [ j-W[ i ] ] + V[ i ]),它是在上面两种情况下取最大值,这也是为什么上面强调假设的原因,它表明的意义是即使物品 i 重量不大于背包总容量,也并不一定是非选它不可, 具体还要看能不能从前 i-1 件中选出更大的价值。

从大方程可以看出,背包容量 j 下前 i 件物品中的最优解,只和背包容量 j 下前 i-1 件物品中的最优解和背包容量减去物品 i 重量下前 i-1 件物品中的最优解有关。因此,可以通过循环迭代将不同情况限制下的最优解填写成一张表,类似下图,并可以根据这张表直接得到不同限制情况下的最优解。
在这里插入图片描述
物品编号和背包容量都含0是为了编程方便,并根据表示的实际意义,物品编号为0或背包容量为0时的最大价值应该设为0。

同样下面代码中W,V数组传入时也要将第一位先设为0。比如上图对应的W数组:{0,7,5,4,8,12},V数组:{0,13,8,20,11,6},但这里设0只是起到了占位的作用,也可以设为其它数字。

public class Knapsack_01 {
	public static int knapsack(int[] W, int[] V, int C) {
		// 创建二维数组用来保存不同限制条件下的最优解
		// 注意dp[0][],dp[][0]虽然没有显式初始化0但由于是int数组,默认都为0
		int[][] dp = new int[W.length][C + 1];
		for (int i = 1; i < W.length; i++) {
			for (int j = 1; j < C + 1; j++) {
				if (W[i] > j) {
					dp[i][j] = dp[i - 1][j];
				} else {
					dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - W[i]] + V[i]);
				}
			}
		}
		// 数组最右下角就是最大价值
		return dp[W.length - 1][C];
	}

	public static void main(String[] args) {
		int[] W = { 0, 7, 5, 4, 8, 12 };
		int[] V = { 0, 13, 8, 20, 11, 6 };
		System.out.println(knapsack(W, V, 20));
	}
}

设物品个数 n,背包最大容量 m,因双层循环所以上面代码的时间复杂度 O ( n ∗ m ) Ο(n*m) O(nm),空间复杂度即二维数组的空间也是 O ( n ∗ m ) Ο(n*m) O(nm)

dp优化(一维数组)

优化主要是对二维数组的优化,从上面的状态转移方程可以看出,下一行的数据只与上一行的数据有关,更确切的说, 任意 F[ i ] [ j ] 的结果只与它上一行左边的 F[ i-1 ] [ 0 ] ~ F[ i-1 ] [ j ] 这一部分区间数据相关。利用这一特点可以将二维数组优化为一维数组。

优化过程网上看了很多都没有讲的太详细,理解了好久~

如果上面的dp数组是保留2行的话,相信很多人都知道怎么优化,可以通过滚动数组来解决。其实2行1行道理都差不多,上面说了,对于任意 F[ i ] [ j ] 的结果只与它上一行左边的这一部分区间数据相关,还是这张图
在这里插入图片描述
假设以物品序号和背包容量为坐标描述的话,(1,20)位置的值13只与(0,0)~(0,20)处的数据有关,对于第一行的数据填写时,从右往左填写,而得到的值并不是直接写在(1,20)的位置上,而是写在上一行相同的位置(0,20),为什么可以这么写?因为下次确定(1,19)处的值的时候已经用不上这个位置的数据了。

因此,虽然只有一行数据,但却包括了逻辑上的两行,每次所填写的位置左边的数据都属于上一行的数据,填写位置及右边的数据都属于下一行,理解了这,就能明白为什么不能从左往右填写:因为第二行新的数据会覆盖第一行可能后面还要参与计算的数据。

这样从右往左填写一遍便可以得到第一行的所有数据,类似的得到第二行,第3行…,直到最后一行,而事实上需要的也仅是最后一行的最后一个位置上的数字。

优化后新的状态转移方程:F[ j ] = max( F[ j ] ,F[ j-W[ i ] ] + V[ i ] ),
方程中括号外面的 F[ j ] 相当于原方程中的 F[ i ] [ j ],
括号里面的 F[ j ] 相当于原方程中的 F[ i-1 ] [ j ],
括号里面的 F[ j-W[ i ] ] + V[ i ] 相当于原方程中的 F[ i-1 ] [ j-W[ i ] ] + V[ i ]。
对比原新方程可以发现,区别就是少了表示行数的 i 。

优化后的代码

public class Knapsack_01 {
	public static int knapsack(int[] W, int[] V, int C) {
		int[] dp = new int[C + 1];
		for (int i = 1; i < W.length; i++) {
			for (int j = C; j >= 1; j--) {
				if (W[i] < j)
					dp[j] = Math.max(dp[j], dp[j - W[i]] + V[i]);
			}
		}
		return dp[C];
	}

	public static void main(String[] args) {
		int[] W = { 0, 7, 5, 4, 8, 12 };
		int[] V = { 0, 13, 8, 20, 11, 6 };
		System.out.println(knapsack(W, V, 20));
	}
}

优化后的时间复杂度不变还是 O ( n ∗ m ) Ο(n*m) O(nm),而空间复杂度减小到了 O ( m ) Ο(m) O(m)

根据dp数组获取选择的物品编号

将没有优化过的状态转移方程反向运用

int j = C;
for (int i = W.length - 1; i >= 1; i--) {
	if (dp[i][j] == dp[i - 1][j - W[i]] + V[i]) {
		System.out.print("物品" + i + " ");
		j = j - W[i];
	}
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值