01背包问题详解 具体样例模拟版

发布于:2025-04-08 ⋅ 阅读:(23) ⋅ 点赞:(0)

01背包

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0< v i v_i vi, w i w_i wi≤1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

01背包问题,最基础的背包问题,每件物品只有一件,可以选择放或不放

状态转移方程为 f[i][j] = max(f[i - 1][j] , f[i - 1][j - v[i]] + w[i])

其中**f[i][j]**是前i件物品,背包容量是j的最大价值

当选择不放第i件物品时,那么问题就转化为 前i-1件物品放入容量为v的背包中 ,即f[i][j] = f[i - 1][j]

当选择放第i件物品时,那么问题就变成了 前i-1件物品放入剩下的容量为v-c[i]的背包中 ,即f[i][j] = f[i - 1][j - v[i]] + w[i]

代码如下

import java.io.*;
import java.util.*;
public class Main {
    static final int N = 1010;
    static int[] volume = new int[N];
    static int[] value = new int[N];
    static int[][] maxValue = new int[N][N]; 
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int v = sc.nextInt();
        for(int i = 1; i <= n; i++) {
            volume[i] = sc.nextInt();
            value[i] = sc.nextInt();
        }
        //计算最大价值
        for(int i = 1; i <= n; i++) {
            for(int j = 0; j <= v; j++) {
                maxValue[i][j] = maxValue[i - 1][j];
                if(j >= volume[i]) {
                     maxValue[i][j] =  Math.max(maxValue[i - 1][j], maxValue[i - 1][j - volume[i]] + value[i]);
                }
            }
        }
        System.out.println(maxValue[n][v]);
        sc.close();
    }
}

下面通过一个具体的例子模拟一下整个过程

有三个物品,编号为1、2、3,其体积和价值如下表,背包的容积是10

编号 体积 价值
1 4 5
2 7 9
3 5 6

以下过程十分详细,但看起来也很恶心,如果已经理解了整个过程,可以直接看后面的一维优化


当 i = 1 (考虑物品 1)
外层循环 i 代表当前考虑的物品编号,内层循环 j 代表当前背包容量。
当 j = 0 :
maxValue[1][0] = maxValue[0][0] = 0 ,因为 j < volume[1] ,不放入物品 1。
当 j = 1 :
maxValue[1][1] = maxValue[0][1] = 0 ,因为 j < volume[1] ,不放入物品 1。
当 j = 2 :
maxValue[1][2] = maxValue[0][2] = 0 ,因为 j < volume[1] ,不放入物品 1。
当 j = 3 :
maxValue[1][3] = maxValue[0][3] = 0 ,因为 j < volume[1] ,不放入物品 1。
当 j = 4 :
maxValue[1][4] = Math.max(maxValue[0][4], maxValue[0][4 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 5 :
maxValue[1][5] = Math.max(maxValue[0][5], maxValue[0][5 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 6 :
maxValue[1][6] = Math.max(maxValue[0][6], maxValue[0][6 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 7 :
maxValue[1][7] = Math.max(maxValue[0][7], maxValue[0][7 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 8 :
maxValue[1][8] = Math.max(maxValue[0][8], maxValue[0][8 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 9 :
maxValue[1][9] = Math.max(maxValue[0][9], maxValue[0][9 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
当 j = 10 :
maxValue[1][10] = Math.max(maxValue[0][10], maxValue[0][10 - 4] + value[1]) = Math.max(0, 0 + 5) = 5 ,可以放入物品 1。
此时 maxValue[1] 数组的值为 [0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5] 。
当 i = 2 (考虑物品 2)
当 j = 0 :
maxValue[2][0] = maxValue[1][0] = 0 ,因为 j < volume[2] ,不放入物品 2。
当 j = 1 :
maxValue[2][1] = maxValue[1][1] = 0 ,因为 j < volume[2] ,不放入物品 2。
当 j = 2 :
maxValue[2][2] = maxValue[1][2] = 0 ,因为 j < volume[2] ,不放入物品 2。
当 j = 3 :
maxValue[2][3] = maxValue[1][3] = 0 ,因为 j < volume[2] ,不放入物品 2。
当 j = 4 :
maxValue[2][4] = maxValue[1][4] = 5 ,因为 j < volume[2] ,不放入物品 2。
当 j = 5 :
maxValue[2][5] = maxValue[1][5] = 5 ,因为 j < volume[2] ,不放入物品 2。
当 j = 6 :
maxValue[2][6] = maxValue[1][6] = 5 ,因为 j < volume[2] ,不放入物品 2。
当 j = 7 :
maxValue[2][7] = Math.max(maxValue[1][7], maxValue[1][7 - 7] + value[2]) = Math.max(5, 0 + 9) = 9 ,可以放入物品 2。
当 j = 8 :
maxValue[2][8] = Math.max(maxValue[1][8], maxValue[1][8 - 7] + value[2]) = Math.max(5, 0 + 9) = 9 ,可以放入物品 2。
当 j = 9 :
maxValue[2][9] = Math.max(maxValue[1][9], maxValue[1][9 - 7] + value[2]) = Math.max(5, 0 + 9) = 9 ,可以放入物品 2。
当 j = 10 :
maxValue[2][10] = Math.max(maxValue[1][10], maxValue[1][10 - 7] + value[2]) = Math.max(5, 0 + 9) = 9 ,可以放入物品 2。
此时 maxValue[2] 数组的值为 [0, 0, 0, 0, 5, 5, 5, 9, 9, 9, 9] 。
当 i = 3 (考虑物品 3)
当 j = 0 :
maxValue[3][0] = maxValue[2][0] = 0 ,因为 j < volume[3] ,不放入物品 3。
当 j = 1 :
maxValue[3][1] = maxValue[2][1] = 0 ,因为 j < volume[3] ,不放入物品 3。
当 j = 2 :
maxValue[3][2] = maxValue[2][2] = 0 ,因为 j < volume[3] ,不放入物品 3。
当 j = 3 :
maxValue[3][3] = maxValue[2][3] = 0 ,因为 j < volume[3] ,不放入物品 3。
当 j = 4 :
maxValue[3][4] = maxValue[2][4] = 5 ,因为 j < volume[3] ,不放入物品 3。
当 j = 5 :
maxValue[3][5] = Math.max(maxValue[2][5], maxValue[2][5 - 5] + value[3]) = Math.max(5, 0 + 6) = 6 ,可以放入物品 3。
当 j = 6 :
maxValue[3][6] = Math.max(maxValue[2][6], maxValue[2][6 - 5] + value[3]) = Math.max(5, 0 + 6) = 6 ,可以放入物品 3。
当 j = 7 :
maxValue[3][7] = Math.max(maxValue[2][7], maxValue[2][7 - 5] + value[3]) = Math.max(9, 5 + 6) = 11 ,可以放入物品 3。
当 j = 8 :
maxValue[3][8] = Math.max(maxValue[2][8], maxValue[2][8 - 5] + value[3]) = Math.max(9, 5 + 6) = 11 ,可以放入物品 3。
当 j = 9 :
maxValue[3][9] = Math.max(maxValue[2][9], maxValue[2][9 - 5] + value[3]) = Math.max(9, 5 + 6) = 11 ,可以放入物品 3。
当 j = 10 :
maxValue[3][10] = Math.max(maxValue[2][10], maxValue[2][10 - 5] + value[3]) = Math.max(9, 5 + 6) = 11 ,可以放入物品 3。
此时 maxValue[3] 数组的值为 [0, 0, 0, 0, 5, 6, 6, 11, 11, 11, 11] 。


在这里插入图片描述

观察这个二维状态转移方程可以发现,maxValue[i][j] 的计算只依赖于 maxValue[i - 1][j]maxValue[i - 1][j - volume[i]],也就是说,当前状态 i 只和上一个状态 i - 1 有关。因此,我们可以只使用一个一维数组 maxValue[j] 来保存状态,在每一轮更新时,直接覆盖上一轮的状态,从而将空间复杂度从 (O(nv)) 优化到 (O(v))。

优化后版本

import java.io.*;
import java.util.*;
public class Main {
    static final int N = 1010;
    static int[] volume = new int[N];
    static int[] value = new int[N];
    static int[] maxValue = new int[N]; 
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int v = sc.nextInt();
        for(int i = 1; i <= n; i++) {
            volume[i] = sc.nextInt();
            value[i] = sc.nextInt();
        }
        //计算最大价值
        for(int i = 1; i <= n; i++) {
            for(int j = v; j >= volume[i]; j--) {
                maxValue[j] = Math.max(maxValue[j], maxValue[j - volume[i]] + value[i]);
            }
        }
        System.out.println(maxValue[v]);
    }
}

到这肯定会有这样的疑问:为什么内层循环要从大到小枚举

因为如果从小到大枚举的话,j - volume[i] 严格小于 j ,也就是maxValue[j - volume[i]] 可能已经在这一次中被更新过了

将其复原的话会变成 maxValue[i][j] = Math.max(maxValue[i][j], maxValue[i][j - volume[i]] + value[i]);

而不是i - 1

还是按照上面的例子详细说明:

当 i = 1 时,物品 1 的重量 volume[1] = 4,价值 value[1] = 5。

如果内层循环从小到大枚举
当 j = 4 时:
maxValue[4] = Math.max(maxValue[4], maxValue[4 - 4] + value[1]) = Math.max(0, 0 + 5) = 5
当 j = 5 时:
maxValue[5] = Math.max(maxValue[5], maxValue[5 - 4] + value[1]) = Math.max(0, 0 + 5) = 5
当 j = 8 时:
maxValue[8] = Math.max(maxValue[8], maxValue[8 - 4] + value[1]),此时 maxValue[8 - 4] 已经在 j = 4 时被更新为放入物品 1 后的状态,即 maxValue[4] = 5,所以 maxValue[8] = Math.max(0, 5 + 5) = 10。这就意味着我们在计算 maxValue[8] 时,又一次使用了物品 1,相当于物品 1 被重复放入了背包,违背了 0 - 1 背包问题每个物品只能使用一次的规则。

当 i = 1 时,物品 1 的重量 volume[1] = 4,价值 value[1] = 5。

如果内层循环从大到小枚举:
当 j = 10 时:
maxValue[10] = Math.max(maxValue[10], maxValue[10 - 4] + value[1]) = Math.max(0, 0 + 5) = 5
当 j = 9 时:
maxValue[9] = Math.max(maxValue[9], maxValue[9 - 4] + value[1]) = Math.max(0, 0 + 5) = 5
当 j = 8 时:
maxValue[8] = Math.max(maxValue[8], maxValue[8 - 4] + value[1]),此时 maxValue[8 - 4] 还没有被更新,仍然是上一轮(即没有考虑物品 1)的状态,所以 maxValue[8] = Math.max(0, 0 + 5) = 5。
通过从大到小枚举,我们保证了在计算 maxValue[j] 时,maxValue[j - volume[i]] 是上一轮(即 i - 1 )的状态,避免了同一个物品被重复使用的问题,从而正确地实现了 0 - 1 背包问题的优化。
综上所述,内层循环从大到小枚举可以避免同一个物品被重复使用的问题,保证了状态转移的正确性。