题目来源:背包九讲
时间限制:1000ms 内存限制:64mbjava
有 \(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。
第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可以使这些物品的整体积不超过背包容量,且总价值最大。
输出最大价值。算法
第一行两个整数,\(N\),\(V\),用空格隔开,分别表示物品数量和背包容积。
接下来有 \(N\) 行,每行两个整数 \(v_i\),\(w_i\),用空格隔开,分别表示第 \(i\) 件物品的体积和价值。shell
输出一个整数,表示最大价值。数组
0 < \(N\),\(V\) ≤ 1000
0 < \(v_i\),\(w_i\) ≤ 1000学习
4 5 1 2 2 4 3 4 4 5
8
尝试各类可能的商品组合,并找出价值最高的组合。
使用 \(N\) 位二进制字串表示物品是否放入背包,枚举全部的可能,而后算出每种可能的价值,取其最大值输出。
解法不足:速度很是慢。在只有3件商品的状况下,你须要计算8个不一样的集合;当有4件商品的时候,你须要计算16个不一样的集合。每增长一件商品,须要计算的集合数都将翻倍。
对于每一件商品,都有选或不选两种可能,即这种算法的运行时间是 \(O(2^n)\) 。
IO竞赛(例如:蓝桥杯)当中,若是实在想不起来动态规划,可使用这个方法拿到一部分分数,可是在ACM竞赛当中,就不能使用这种方法了。ui
import java.util.*; public class Main { static int getSum(int n, int v, StringBuilder binString, int[][] items) { int sumV = 0, sumN = 0; for (int j = 0; j < n; j++) { if (binString.charAt(j) == '1') { sumV += items[j][0]; sumN += items[j][1]; } if (sumV > v) { return -1; } } return sumN; } public static void main(String[] args) { Scanner input = new Scanner(System.in); int n = input.nextInt(); int v = input.nextInt(); int totalV = 0, totalN = 0; int[][] items = new int[n][2]; for (int i = 0; i < n; i++) { items[i][0] = input.nextInt(); items[i][1] = input.nextInt(); totalV += items[i][0]; totalN += items[i][1]; } input.close(); if (totalV <= v) { System.out.println(totalN); return; } int sumN = 0; for (long i = 0; i < Math.pow(2, n); i++) { StringBuilder binString = new StringBuilder(Long.toBinaryString(i)); long len = binString.length(); while (len < n) { binString.insert(0, "0"); len++; } sumN = Math.max(sumN, getSum(n, v, binString, items)); } System.out.println(sumN); } }
对于动态规划算法,能够去这篇文章里学习:经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解)
每一个动态规划都从一个网格开始。
在本题中,网格的各行表示商品,各列表明不一样容量的背包(从1到V)。
网格最初是空的。你将填充其中的每一个格子,网格填满后,就找到了问题的答案!
好比本题样例的网格以下图:
spa
在每一个一格子你都要作出一个选择:放不放进这一行对应的物品。
物品1所占空间为 \(1\) ,也就是说物品1能放进容量为 \(1\) 的这个背包,所以这个单元格包含物品1。因此往第一行第一列中装入物品1,价值为2。
这行的其余单元格也同样。别忘了,这是第一行,只有一个物品可供你选择,换而言之,你伪装如今尚未打算放进其余物品。
填充完以后以下图:
此时你极可能心存疑惑:原题说的是容量为5的背包,咱们为什么要考虑容量为一、二、三、4的背包呢?动态规划从子问题着手,逐步解决大问题。这里解决的子问题将帮助你解决大问题。
这行表示的是当前的最大价值,并非最终解。随着算法往下执行,将逐步修改最大价值。.net
在第二行中,你有两种物品选择。先看第一个单元格,容量为1,以前装进去的最大的价值为2。
这一个单元格该不应放入第二个物品呢?答案显然是不行,由于容量为1的口袋没法装下占空间2的物品。
所以第一列的最大价值保持不变。
接下来看第二行第二列,这个格子所对应背包的容量为2,如今可以装下第二个物品了,对比一下价值,比以前决定的物品1价值高,因此将原来的物品1换为物品2。
再看后面的第三列,这个格子容量为3,能够同时装下物品1和物品2,因此都放进去。
以后的列也进行同样的操做,最后操做完第二行的结果为:
3d
以一样的方式处理物品3,物品3占用空间3,价值为4。
其中在第五列时,容量为5,本来决定的为(物品1+物品2),如今有物品3能够选择了,因此考虑将物品1换为价值更高的物品3,因此此行第五列结果为8
作完这一步就获得了下面的网格:
code
能够总结获得一个公式:\( cell[i][j](i和j代指行和列) = 二者中较大的一项 \begin{cases} 1.上一个单元格的值(即:cell[i-1][j]的值) \\ 2.当前物品的价值+剩余空间的价值(即:cell[i-1][j-当前物品的占用空间] + 当前物品的价值) \end{cases} \)
你可使用这个公式来计算每一个单元格的价值,最终的网格将与前一个网格相同。
这里回答前面抛出的问题:为何要求解子问题?——由于你能够合并两个子问题的解来获得更大问题的解。
相信看到这里,而且亲手推导过网格,应该对动态规划的状态转移方程背后的逻辑有了更深的理解。如今,再回头看01背包问题的经典描述,并实现代码。
声明一个数组dp[n+1][v+1]表示初始网格,首行为0,表示不放入任何物品,同时也为了代码阅读性,从下标1开始处理。
根据第五步的公式,对于编号为 \(i\) 的物品:
import java.util.*; public class Main { static int maxValue(int n, int v, int[][] items) { if (n == 0) { return 0; } int[][] dp = new int[n + 1][v + 1]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= v; j++) { int valueWith_i = (j - items[i - 1][0] >= 0) ? (items[i - 1][1] + dp[i - 1][j - items[i - 1][0]]) : 0; int valueWithout_i = dp[i - 1][j]; dp[i][j] = Math.max(valueWith_i, valueWithout_i); } } return dp[n][v]; } public static void main(String[] args) { Scanner input = new Scanner(System.in); int n = input.nextInt(); int v = input.nextInt(); int totalV = 0, totalN = 0; int[][] items = new int[n][2]; for (int i = 0; i < n; i++) { items[i][0] = input.nextInt(); items[i][1] = input.nextInt(); totalV += items[i][0]; totalN += items[i][1]; } input.close(); if (totalV <= v) { System.out.println(totalN); return; } System.out.println(maxValue(n, v, items)); } }