动态规划——01背包问题

问题描述:给定 N 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。

:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

题目分析从N种物品中选择m种商品放入容量为C的背包,v1+v2+v3+……+vm最大,即求最优解,这里我们使用动态规划来解决该问题。题目需要求解背包容量为C时的最优解,那么我们可以分解问题,让背包容量从1增加到C,依次求解容量为1时的最优解、容量为2时的最优解;那么基于动态规划求最优解的性质——将子问题的最优解集合之后就是总问题的最优解,我们即可求得背包问题的最优解。

动态规划:动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。

实现思路:声明一个数组table[N+1][C+1],table[i][j]表示在背包容量为 j 、选择第 i 件商品的时候所能获得商品的最大价值。现在我们需要实现一个函数 initTable(int[] value, int[] weight, int N, int capacity),该函数用来初始化这个table,value[i]数组表示第 i 个商品的价值(i 从1开始计算),weight[i]  表示第 i 个商品的权值,即所占背包的容量。

面对一个商品,我们有装入背包和不装入背包两种选择,那么我们来分析一下table[i][j] 的计算方法。

(1) j < w[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿:m[ i ][ j ] = m[ i-1 ][ j ]

(2) j >= w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。

        拿:table[i ][j]  = table[ i-1 ][ j - weight[i] ] + value[ i ]

        不拿:table[i ][j] = table[ i - 1 ][ j ]

我们需要比较两种情况下的table[i ][j]值,做出相应选择。

现在我们写出实现代码:

/**
	 * 初始化table,table右下角的值即为最优解
	 * @param value 价值数组
	 * @param weight 权值数组
	 * @param N 商品种类
	 * @param capacity 背包容量
	 * @return table
	 */
	private static int[][] initTable(int[] value, int[] weight, int N, int capacity) {
		if(value.length != weight.length) {
			System.out.println("参数value、weight有误");
			return null;
		}
		int[][] table = new int[N + 1][capacity + 1];
		for(int i = 1; i <= N; i++) {
			for(int j = 1; j <= capacity; j++) {
				if(j < weight[i]) {//装不下
					table[i][j] = table[i-1][j];
				} else {//能装下
					if(table[i-1][j] > (table[i-1][j-weight[i]] + value[i])) {//不装i价值更大
						table[i][j] = table[i-1][j];
					} else {//装了i价值更大
						table[i][j] = table[i-1][j-weight[i]] + value[i];
					}
				}
			}
		}
		
		return table;
	}

这样就可以得到最优解了,最优解是table[N] [ capacity ]。可是,我们还想获得选择了哪几种商品,这又该怎么实现呢?这里我们知道了最优解,那么我们可以使用逆向思维来思考一下, 最优解是 table[ i ] [ j ] ,那么table[ i ] [ j ] = table[ i -1 ] [ j ] 或者 table[ i - 1 ] [ j  -  weight [i] ] + value[i], 我们可以根据初始化table的思路来进行求解:

             1) table[ i ][ j ] = table[ i -1 ] [ j ] 时,说明没有选择第i 个商品,则回到V(i-1,j);

             2)table[ i ][ j ] = table[ i - 1 ] [ j  -  weight [i] ] + value[i]时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));

       3) 一直遍历到 i  < 0结束为止,所有解的组成都会找到。

代码如下:

	/**
	 * 通过回溯获得选择的商品:根据最优解回溯找出解的组成
	 * @param i 
	 * @param j
	 * @param table 信息记录表
	 * @param weight 权值数组
	 * @param value 价值数组
	 * @param result 返回结果数组
	 */
	private static void findChoice(int i, int j, int[][] table, int[] weight, int[] value, int[] result) {
		if(i > 0) {
			if(table[i][j] == table[i-1][j]) {//没有选择第i个商品
				result[i] = 0;
				findChoice(i-1, j, table, weight, value, result);
			} else if(table[i][j] == (table[i-1][j-weight[i]] + value[i])) {//选择了第i个商品
				result[i] = 1;
				findChoice(i-1, j-weight[i], table, weight, value, result);
			}
		}
	}

main函数测试如下:

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int N = 0;
		int capacity;
		int[] value;
		int[] weight;
		//初始化数据
		//输入格式: 4 8//输入商品种类(N)和背包容量(capacity)
		//         2 3 4 5//输入每类商品的权值(占用背包容量的值)
		//         3 4 5 6//输入每类商品的价值
		N = sc.nextInt();
		capacity = sc.nextInt();
		if(N <= 0) {
			System.out.println("输入参数N有误");
			return;
		}
		value = new int[N+1];
		weight = new int[N+1];
		for(int i = 1; i <= N; i++) {//初始化权值数组
			weight[i] = sc.nextInt();
		}
		for(int i = 1; i <= N; i++) {//初始化价值数组
			value[i] = sc.nextInt();
		}
		
		
		int[][] table = initTable(value, weight, N, capacity);
		System.out.println("最优解:" + table[N][capacity]);
		int[] choice = new int[N+1];
		findChoice(N, capacity, table, weight, value, choice);
		System.out.println("选择商品数组,1表示选择,0表示没有选择:" + Arrays.toString(choice));
		sc.close();
	}


猜你喜欢

转载自blog.csdn.net/kangxidagege/article/details/80136212