背包问题
背包问题
1、0-1背包
你有一个载重为M的背包,现在有N个物品,每个物品重量为weight[i],价值为value[i],求在不超出载重的情况下,能取到的最大价值为多少?
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int n = scanner.nextInt();
int[][] result = new int[n+1][m+1];
int[] weight = new int[n+1];
int[] value = new int[n+1];
for(int i = 1;i < n;i++){
weight[i] = scanner.nextInt();
}
for(int i = 1;i < n;i++){
value[i] = scanner.nextInt();
}
for(int i = 1;i <= n;i++){
for(int k = 1;k <= m;k++){
if(k >= weight[i]){
result[i][k] = Math.max(result[i-1][k],result[i-1][k-weight[i]] + value[i]);
}else{
result[i][k] = result[i-1][k];
}
}
}
System.out.println(JSON.toJSONString(result));
}
上述流程可以简化空间
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int n = scanner.nextInt();
int[] result = new int[m+1];
int[] weight = new int[n+1];
int[] value = new int[n+1];
for(int i = 1;i < n;i++){
weight[i] = scanner.nextInt();
}
for(int i = 1;i < n;i++){
value[i] = scanner.nextInt();
}
for(int i = 1;i <= n;i++){
for(int k = m;k >= 1;k--){
if(k >= weight[i]){
result[k] = Math.max(result[k],result[k-weight[i]] + value[i]);
}
}
}
System.out.println(JSON.toJSONString(result));
}
我们回来来看0-1背包问题,为了计算出指定背包容量的最优值,我们定义一个状态数组,dp[i][j]=maxValue,我们用行标i表示这次尝试放入下标是i的物品,而列标j代表当前的背包的容量,而值maxValue代表将前i个物品放入背包容量为j的背包中所能获取的最大值。当然这个状态的挑选本身并不容易想,这个状态的值决定了在j容量的前提下前i物品的选择最优值,也就是我们说的局部最优,并且它是确定的,不会因为后面的选择而改变。有了这个状态值的定义,我们就可以类似于数学归纳法一样,首先确定初值,然后层层递进求解。
那么初值是什么呢?初值是不放物品的情况下,即i为0的情况下,不论背包容量j有多大,我们的maxValue始终是0,即dp[0][j]=0,这个就是初值。
有了初值后,我们就可以利用这个初值状态计算dp[1][j],dp[2][j]等等,我们需要推出一个通用的公式来求dp[i][j],这样我们就可以用循环来进行迭代计算。要算dp[i][j],也就是尝试将第i个物品放入容量为j的背包,那么我们有两种选择,将i放入背包后的价值and不放入i的价值,我们只有这两种选择,这是个二分选择问题。如果我们将i放入背包,那么肯定要预留i的位置,那么放入i后的价值是dp[i-1][j-w[i]]+v[i],w[i],v[i]代表i的重量和价值,其中dp[i-1][j-w[i]]是已经计算好的前i-1个物品放入背包容量减去i重量的最大价值,这个是子最优值。另一方面,如果不放入i物品,前i个物品的价值就取前i-1个物品的最优值,即dp[i-1][j]。最后dp[i][j]取这两者的最大值,也就是这次选择的最优值,即dp[i][j]=max{dp[i-1][j-w[i]]+v[i],dp[i-1][j]}。
这个看着复杂的式子就是我们进行迭代计算的根本,后一个的最优值是基于前一个的最优值计算的,如此层层迭代,最终得到我们的结果。回过头来,我们想想这种动态规划的方法为什么会得到全局最优,而贪婪算法却不能,从过程中来看,我们每层的计算都是基于前一层的最优值,并且在最优值上做了二分选择,这是和贪婪策略完全不同之处。
2、完全背包
完全背包问题同样是有N种物品和一个容量为C的背包,和0-1背包不同的是每种物品的个数是无限个。这种情况下,其实我们可以将完全背包问题转换成0-1背包问题。
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int n = scanner.nextInt();
int[] result = new int[m+1];
int[] weight = new int[n+1];
int[] value = new int[n+1];
for(int i = 1;i <= n;i++){
weight[i] = scanner.nextInt();
}
for(int i = 1;i <= n;i++){
value[i] = scanner.nextInt();
}
for(int i = 1;i <= n;i++){
for(int k = m;k >= 1;k--){
for(int j = 0;j * weight[i] <= k;j++){
result[k] = Math.max(result[k],result[k-weight[i]*j] + value[i]*j);
}
}
}
System.out.println(JSON.toJSONString(result));
}
不过这会增大循环的过程,使得算法复杂度升高。我们考虑使用另一种方法。在上面的空间优化过的0-1背包问题中,我们在第二个循环中,使用了倒序更新的方式,原因是我们在要保证上一层更新dp[j]和dp[j-w[i]]是和dp[i-1][j],dp[i-1][j-w[i]],而当每个物品的个数是无限制的情况下,我们的更新需要依赖于我们当前的更新状态,一个大容量的背包可能同时放入多个当前物品。
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int n = scanner.nextInt();
int[] result = new int[m+1];
int[] weight = new int[n+1];
int[] value = new int[n+1];
for(int i = 1;i < n;i++){
weight[i] = scanner.nextInt();
}
for(int i = 1;i < n;i++){
value[i] = scanner.nextInt();
}
for(int i = 1;i <= n;i++){
for(int k = 1;k <= m;k++){
if(k >= weight[i]){
result[k] = Math.max(result[k],result[k-weight[i]] + value[i]);
}
}
}
System.out.println(JSON.toJSONString(result));
}
3、多重背包
多重背包问题是介于0-1背包和完全背包问题之间,多重背包的每个物品的数量是有限的,是介于1到无限之间的某个数。当然我们仍然可以使用0-1背包问题来解决,假设我们有N种物品,第i个物品的个数是ki,背包容量是C,那我们的时间复杂度就变成了C*(所有k的和)。
如果想要削减时间复杂度,一种方法是将每个k按二进制切分,比如如果数量是7,我们切分成1,2,4三种物品,然后用0-1背包的问题进行求解,这样,时间复杂度就变成了C*(所有log(k)的和)。这样划分可以的原因在于我们可以用通过二进制换分的组合表示一个这个数范围内的所有数。
浙公网安备 33010602011771号