问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
题目分析:从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(); }