lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

1.  什么是01背包问题?

01背包问题是一种经典的组合优化问题,它的描述如下:

有n种物品和一个容量为C的背包,每种物品有一个重量w[i]和一个价值v[i],其中i=1,2,…,n。问如何选择物品放入背包,使得背包内的物品总价值最大,且不超过背包的容量?

这里的01表示每种物品只能选择放入或不放入,不能分割或重复。

2.   为什么可以用动态规划解决?

动态规划是一种解决复杂问题的方法,它的核心思想是将一个大问题分解为若干个子问题,然后自底向上地求解子问题,并将子问题的解保存起来,避免重复计算。最后,通过子问题的解组合出原问题的解。

动态规划适用于具有最优子结构和重叠子问题的问题。最优子结构指的是一个问题的最优解可以由其子问题的最优解构成;重叠子问题指的是在求解过程中,有些子问题会被多次遇到和求解。

01背包问题就具有这两个特点。首先,我们可以将原问题分解为n个阶段,每个阶段对应一种物品。在每个阶段,我们需要决定是否将该物品放入背包。如果我们已经知道了前i-1个阶段的最优解,那么第i个阶段的最优解就可以由以下两种情况中的较大者得到:

  • 如果第i种物品的重量w[i]大于当前背包的剩余容量j,那么我们不能放入该物品,此时第i个阶段的最优解就等于前i-1个阶段的最优解,即V(i,j)=V(i-1,j);
  • 如果第i种物品的重量w[i]小于等于当前背包的剩余容量j,那么我们可以选择放入或不放入该物品。如果不放入,那么第i个阶段的最优解仍然等于前i-1个阶段的最优解,即V(i,j)=V(i-1,j);如果放入,那么第i个阶段的最优解等于前i-1个阶段在容量为j-w[i]时的最优解加上第i种物品的价值v[i],即V(i,j)=V(i-1,j-w[i])+v[i]。因此,在这种情况下,我们需要在这两种选择中取较大者作为第i个阶段的最优解,即V(i,j)=max{V(i-1,j),V(i-1,j-w[i])+v[i]}。

这样,我们就得到了一个递推关系式,它表明了原问题的最优解可以由其子问题的最优解构成,即具有最优子结构。

其次,在求解过程中,我们会遇到很多相同或类似的子问题。例如,在求解V(i,j)时,我们需要知道V(i-1,j)和V(i-1,j-w[i]);而在求解V(i+1,j)时,我们又需要知道V(i,j)和V(i,j-w[i+1])。这些子问题都涉及到前面某些阶段在某些容量下的最优解。如果我们每次都重新计算这些子问题,那么会造成很多重复的工作。为了避免这种情况,我们可以用一个二维数组来保存子问题的解,每次遇到一个子问题,我们先查看数组中是否已经有该子问题的解,如果有,就直接使用;如果没有,就按照递推关系式计算,并将结果存入数组中。这样,我们就可以利用已经求解过的子问题的解,避免重复计算,即利用了重叠子问题。

3.   如何用动态规划求解?

根据上面的分析,我们可以用以下步骤来求解01背包问题:

  • 定义一个二维数组dp[n+1][C+1],其中dp[i][j]表示在前i种物品和容量为j的背包下的最优解;
  • 初始化边界条件,即dp[0][j]=0(没有物品时,最优解为0)和dp[i][0]=0(没有容量时,最优解为0);
  • 从第一种物品开始,遍历每一种物品i;
  • 对于每一种物品i,从第一个单位容量开始,遍历每一个单位容量j;
  • 对于每一个单位容量j,根据递推关系式计算dp[i][j];
  • 最后,返回dp[n][C]作为原问题的最优解。

4.  有什么实例可以说明?

假设有4种物品和一个容量为8的背包,每种物品的重量和价值如下表所示:

 

物品编号重量价值
1 2 3
2 3 4
3 4 5
4 5 6

按照动态规划的步骤,我们可以得到以下的二维数组:

i\j012345678
0 0 0 0 0 0 0 0 0 0
1 0 0 3 3 3 3 3 3 3
2 0 0 3 4 4 7 7 7 7
3 0 0 3 4 5 7 8 9 9
4  0 3 5 8  9  10

我们可以看到,在填写第一行时,由于没有物品可选,所以所有的最优解都是0;在填写第二行时,由于只有第一种物品可选,所以只有当容量大于等于2时才能放入该物品,并获得价值为3的最优解;在填写第三行时,由于有两种物品可选,所以当容量大于等于2时可以选择放入或不放入第二种物品,并取较大者作为最优解;以此类推,在填写第四行时,由于有三种物品可选,所以当容量大于等于4时可以选择放入或不放入第三种物品,并取较大者作为最优解。

最后,在填写第五行时,由于有四种物品可选,所以当容量大于等于5时可以选择放入或不放入第四种物品,并取较大者作为最优解。填写完毕后

 

我们可以看到,最右下角的值dp[4][8]就是原问题的最优解,即在四种物品和容量为8的背包下的最大价值为10。如果我们想知道这个最优解是由哪些物品组成的,我们可以根据递推关系式反向推导。由于dp[4][8]=dp[3][8-w[4]]+v[4]=dp[3][3]+6=4+6=10,说明第四种物品被放入了背包,并且在放入之前,背包的剩余容量为3,对应的最优解为4。然后我们继续查看dp[3][3],由于dp[3][3]=dp[2][3-w[3]]+v[3]=dp[2][0]+5=0+5=5,说明第三种物品也被放入了背包,并且在放入之前,背包的剩余容量为0,对应的最优解为0。这时候,我们已经找到了所有被放入背包的物品,即第三种和第四种物品。因此,我们可以得出结论:在四种物品和容量为8的背包下的最大价值为10,且是由第三种和第四种物品组成的。

  

5.   如何用Java实现动态规划算法?

根据上面的步骤和逻辑,我们可以用Java语言来实现动态规划算法。以下是一个可能的代码:

public class Knapsack {

    // 物品的重量
    private int[] w = {2, 3, 4, 5};
    // 物品的价值
    private int[] v = {3, 4, 5, 6};
    // 物品的数量
    private int n = w.length;
    // 背包的容量
    private int C = 8;
    // 动态规划表
    private int[][] dp = new int[n + 1][C + 1];

    // 求解01背包问题
    public void solve() {
        // 初始化边界条件
        for (int i = 0; i <= n; i++) {
            dp[i][0] = 0;
        }
        for (int j = 0; j <= C; j++) {
            dp[0][j] = 0;
        }
        // 填写动态规划表
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= C; j++) {
                if (j < w[i - 1]) {
                    // 第i种物品的重量大于当前背包的剩余容量,不能放入
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 第i种物品的重量小于等于当前背包的剩余容量,可以选择放入或不放入
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
                }
            }
        }
        // 输出最优解
        System.out.println("最大价值为:" + dp[n][C]);
        // 输出最优解的组成
        System.out.print("最优解的组成为:");
        int j = C;
        for (int i = n; i > 0; i--) {
            if (dp[i][j] > dp[i - 1][j]) {
                // 第i种物品被放入了背包
                System.out.print("物品" + i + " ");
                j = j - w[i - 1];
            }
            if (j == 0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Knapsack knapsack = new Knapsack();
        knapsack.solve();
    }
}

输出结果为:

最大价值为:10
最优解的组成为:物品4 物品3

自行实现如下:

public static void main(String[] args) {


int[] w = {2, 3, 4, 5}; // weight
int[] v = {3, 4, 5, 6}; // value
// 背包的容量
int N = w.length;
int C = 8;
System.out.println(pack(N, C, w, v));
System.out.println(pack2(N, C, w, v));

}
public static int pack(int N, int C, int[] w, int[] v) {

int[][] dp = new int[N + 1][C + 1];
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= C; j++) {
if (j < w[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[N][C];

}

6.  有什么优化和变形的方法?

动态规划算法虽然可以有效地求解01背包问题,但是它也有一些缺点和局限性。例如,它需要一个二维数组来存储子问题的解,这会占用很多空间;它也需要遍历所有的物品和容量,这会消耗很多时间。因此,有一些优化和变形的方法可以提高动态规划算法的效率和适用性。

  • 空间优化:由于动态规划表的每一行只依赖于上一行,所以我们可以用一个一维数组来代替二维数组,从而节省空间。具体地,我们可以用一个长度为C+1的数组dp来表示当前阶段的最优解,然后从后往前更新dp的值,即dp[j]=max{dp[j],dp[j-w[i-1]]+v[i-1]}。这样,我们就不需要保存之前阶段的最优解了,只需要一个一维数组就可以完成动态规划。
  • 时间优化:由于动态规划表的每一行中,只有部分元素会被更新,而且更新的范围是从w[i-1]到C,所以我们可以只遍历这部分元素,从而节省时间。具体地,我们可以用一个变量maxW来记录当前阶段能够达到的最大重量,然后在每次更新dp时,只遍历从w[i-1]到maxW之间的元素,并更新maxW的值为max{maxW,j}。这样,我们就不需要遍历所有的容量了,只需要遍历可能被更新的容量就可以完成动态规划。
  • 变形问题:01背包问题还有一些变形和扩展的问题,例如完全背包问题(每种物品可以无限制地放入)、多重背包问题(每种物品有限定的数量)、分组背包问题(物品分为若干组,每组只能选择一个)、多维背包问题(物品有多种属性,背包有多种限制)等等。这些问题都可以用类似的思路和方法来求解,只是在递推关系式和填表过程中有一些不同。感兴趣的读者可以自行查阅相关资料和代码。

 

7.  一维数组的优化方法

7.1  二维转 一维的两个问题

问题1: 为什么二维可以转化为一维

二维数组的每一行表示在前i种物品和不同容量下的最优解,而每一行的值只依赖于上一行的值,即dp[i][j]只与dp[i-1][j]和dp[i-1][j-w[i-1]]有关。因此,我们不需要保存所有的行,只需要保存当前行和上一行的值就可以了。而由于当前行的值会覆盖上一行的值,所以我们可以用一个一维数组来代替二维数组,只保存当前行的值,即dp[j]表示在当前阶段和容量为j的背包下的最优解。这样,我们就可以节省空间,从而优化动态规划算法。

问题2: 为什么是逆序而不是顺序

如果我们按照顺序更新dp[j]的值,即从j=0到j=C,那么我们会遇到一个问题:当我们更新dp[j]时,我们需要用到dp[j-w[i-1]]的值,但是这个值可能已经被更新过了,不再是上一阶段的值,而是当前阶段的值。这样就会导致错误的结果。例如,在更新第二种物品时,如果我们按照顺序更新dp[3]的值,那么我们会得到dp[3]=max{dp[3],dp[3-3]+4}=max{7,4}=7,这里的dp[3]已经是放入第一种物品后的最优解了,而不是上一阶段的最优解。这样就忽略了不放入第二种物品的情况,导致错误的结果。

为了避免这个问题,我们可以按照逆序更新dp[j]的值,即从j=C到j=w[i-1],这样就可以保证在更新dp[j]时,dp[j-w[i-1]]还是上一阶段的值,没有被覆盖。例如,在更新第二种物品时,如果我们按照逆序更新dp[3]的值,那么我们会得到dp[3]=max{dp[3],dp[3-3]+4}=max{0,4}=4,这里的dp[3]还是上一阶段的最优解,没有被覆盖。这样就可以正确地考虑放入或不放入第二种物品的情况,得到正确的结果。

7.2   推导过程

我们仍然用上面的例子来说明一维数组的优化方法。假设有4种物品和一个容量为8的背包,每种物品的重量和价值如下表所示:

物品编号重量价值
1 2 3
2 3 4
3 4 5
4 5 6

我们定义一个一维数组dp[C+1],其中dp[j]表示在当前阶段和容量为j的背包下的最优解。我们按照以下步骤来更新dp的值:

    • 初始化边界条件,即dp[0]=0(没有容量时,最优解为0);
    • 从第一种物品开始,遍历每一种物品i;
    • 对于每一种物品i,从容量为C开始,递减到w[i-1],遍历每一个单位容量j;
    • 对于每一个单位容量j,根据递推关系式更新dp[j],即dp[j]=max{dp[j],dp[j-w[i-1]]+v[i-1]};
    • 最后,返回dp[C]作为原问题的最优解。

我们可以用以下的表格来表示dp数组的变化过程:

i\j012345678
初始状态                  

我们可以看到,在初始化时,dp数组的所有元素都是0;在更新第一种物品时,由于只有当容量大于等于2时才能放入该物品,并获得价值为3的最优解,所以我们从后往前更新dp[2]到dp[8]的值;在更新第二种物品时,由于有两种物品可选,所以当容量大于等于3时可以选择放入或不放入第二种物品,并取较大者作为最优解,所以我们从后往前更新dp[3]到dp[8]的值;以此类推,在更新第三种和第四种物品时,我们也从后往前更新dp数组的值。最后,我们得到了dp[8]=10作为原问题的最优解。

7.3 方程式

根据上面的推导过程,我们可以得到以下的方程式:

dp[0]=0dp[j]=max{dp[j],dp[jw[i1]]+v[i1]},i=1,2,...,n;j=w[i1],w[i1]+1,...,C

7.4  Java实现

根据上面的方程式,我们可以用Java语言来实现一维数组的优化方法。以下是一个可能的代码:

public class Knapsack {

    // 物品的重量
    private int[] w = {2, 3, 4, 5};
    // 物品的价值
    private int[] v = {3, 4, 5, 6};
    // 物品的数量
    private int n = w.length;
    // 背包的容量
    private int C = 8;
    // 动态规划数组
    private int[] dp = new int[C + 1];

    // 求解01背包问题
    public void solve() {
        // 初始化边界条件
        dp[0] = 0;
        // 遍历每一种物品
        for (int i = 1; i <= n; i++) {
            // 遍历每一个单位容量,从后往前更新
            for (int j = C; j >= w[i - 1]; j--) {
                // 更新dp[j]的值
                dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
            }
        }
        // 输出最优解
        System.out.println("最大价值为:" + dp[C]);
        // 输出最优解的组成
        System.out.print("最优解的组成为:");
        int j = C;
        for (int i = n; i > 0; i--) {
            if (dp[j] > dp[j - w[i - 1]]) {
                // 第i种物品被放入了背包
                System.out.print("物品" + i + " ");
                j = j - w[i - 1];
            }
            if (j == 0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Knapsack knapsack = new Knapsack();
        knapsack.solve();
    }
}

输出结果为:

最大价值为:10
最优解的组成为:物品4 物品3

 

 

自行实现如下:

public static int pack2(int N, int C, int[] w, int[] v) {

int[] dp = new int[C + 1];
for (int i = 1; i <= N; i++) {
for (int j = C; j >= 0; j--) {
if (j >= w[i - 1]) {
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[C];

}

8.  总结

动态规划是一种强大而灵活的算法思想,它可以有效地解决许多组合优化问题。01背包问题是动态规划的经典例子,它可以帮助我们理解动态规划的原理和步骤,并为我们解决其他类似或更复杂的问题提供思路和方法。希望本文能够对您有所帮助。谢谢!

 

 

 

 

 

posted on 2023-07-08 16:31  白露~  阅读(79)  评论(0编辑  收藏  举报