一次性解决三种背包问题

1,639 阅读8分钟

前言

首先,大概讲一下什么是“背包”问题:背包问题是指你有一个容量为V的背包,然后有n个物品在你面前,你要怎么装才能使得背包里的物品总价值最大。而每种物品是只有1个,还是有多个,亦或是有无限个,这就是“01背包”、“多重背包”、“完全背包”的主要区别

这里先打断一下,给自己一点时间,先思考一下这样的区别可能会在解法上有什么不同的区别,接着我们就开始往下看每种背包是怎么解决的。

01背包

假设你是一个小偷,在一个夜黑风高的晚上,偷偷摸摸地进了一个富人的别墅里,看到了有下面三件物品,你心想:美汁汁,这次赚大了。

正准备把这些物品全部打包带走,但是很不巧,这时候外面传来了开门的声音,你来不及打包了,只能将这些物品装进袋子赶紧跑路,这是一个空间大小为4磅的背包,问要怎么选才能使这次的收获最大。

先打断一下,肯定有人会疑惑为什么“01背包”要叫“01背包”,而不是“23背包”或者“45”背包???

很简单,顾名思义,每件物品只能选(1)或者不选(0),选就只能选一个,所以为1,不选就直接为0,所以叫做“01”。

看到这个问题,可能有人会说:那还不简单,直接把笔记本电脑和吉他装进去不就好了。的确如此,但是如果物品一多的时候,你就很难再这么轻松地判断出来了,这时候“动态规划”便应运而生。

接下来,我们开始做选择(也就是暴力递归的思想:尝试所有的选择)。

从上图可以看出,对于每个物品,我们都有两种选择:选/不选,这就产生了一共5种选择,最佳的是0,3500这个选择,接下来我们使用函数来表达一般化的情况。

暴力递归解法

我们先将具体问题转为一般情况的问题,假设有n件物品(x1,x2,...,xn),背包大小为C,每件物品的质量为Wi,价值为Vi,xi取0或1,表示第i个物品取或不取。接着按照暴力递归的三要素来写函数

  1. 函数的定义:f(i, j)表示背包剩余容量为i的时候,前j个物品最佳组合的价值

  2. 递推关系式(即当前调用单元做了什么):对于第j件物品,我们先判断背包剩余容量是否大于当前物品的质量

    2.1 如果装不进,就跳过 f(i, j) = f(i,j-1)
    2.2 装的进,有选或不选两种情况,取总价值大的,对应着f(i, j) = max(f(i,j-1), Vj + f(i - Wj, j - 1))

  3. 递归结束条件:背包剩余容量为0,或者已经遍历完了所有物品

理清楚了上面的三个条件,就很容易写出递归解法了。

public class Solution {

    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {4, 3, 1}; //物品的质量

    public int maximumValue() {
        int result = maximumValueHelper(4, 2);
        return result;
    }

    private int maximumValueHelper(int i, int j) {
        //base case:
        if (j < 0 || i == 0) {
            return 0;
        }

        int result = 0;

        //判断当前容量能否装进第j件物品
        if (i < ws[j]) {
            result = maximumValueHelper(i, j - 1);
        } else {
            //不取第j件物品
            int get = maximumValueHelper(i, j - 1);
            //取第j件物品
            int notGet = vs[j] + maximumValueHelper(i - ws[j], j - 1);
            result = Math.max(get, notGet);
        }
        return result;
    }
}

带记忆数组的递归

在递归过程中,存在着大量的重复计算,所以可以使用一个“记忆数组”来减少重复计算。

public class Solution{
    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {4, 3, 1}; //物品的质量
    //记忆数组:memo[i][j]表示背包剩余容量为i,第0~第j件物品的最佳组合的价值
    int[][] memo = new int[5][3];

    public int maximumValue() {
        int result = maximumValueHelper(4, 2);
        Arrays.fill(memo, -1);
        return result;
    }

    private int maximumValueHelper(int i, int j) {
        //base case:
        if (j < 0 || i == 0) {
            return 0;
        }

        //如果出现过,就直接返回
        if (memo[i][j] != -1) {
            return memo[i][j];
        }

        int result = 0;

        //判断当前容量能否装进第j件物品
        if (i < ws[j]) {
            result = maximumValueHelper(i, j - 1);
        } else {
            //不取第j件物品
            int get = maximumValueHelper(i, j - 1);
            //取第j件物品
            int notGet = vs[j] + maximumValueHelper(i - ws[j], j - 1);
            result = Math.max(get, notGet);
        }
        memo[i][j] = result;
        return result;
    }
}

动态规划

动态规划也是使用一个二维数组来减少重复计算,思路和“带记忆数组的递归”方法类似,不同的 是“动态规划”是自底向上,“带记忆数组的递归”是自顶向下。准备好一个二位数组之后,接下来就是简单的填表过程。

  • 首先第一列dp[0][j],背包空间为0,所以最大价值都是0
  • 接下来其他的格子按照递推式来填

public class Solution {

    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {4, 3, 1}; //物品的质量

    public int maximumValue() {
        int[][] dp = new int[5][3];
        
        //填充第一行
        for (int j = 0; j < dp[0].length; j++) {
            dp[0][j] = 0;
        }

        //接下来的格子按照递推式来填
        for (int i = 1; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                //如果背包剩余容量小于第j件物品的质量
                if (i < ws[j]) {
                    dp[i][j] = dp[i][j - 1];
                } else {
                    dp[i][j] = Math.max(dp[i][j - 1], vs[j] + dp[i - ws[j]][j - 1]);
                }
            }
        }

        return dp[4][2];
    }
}

多重背包

“多重背包”和“01背包”的区别就在于“多重背包”种每个物品的数量有多个,所以在选第j件物品的时候,可以选0个,或者1个,或者在背包容量足够的情况下全选。对照着“01背包”的递推式,我们可以写出“多重背包”的递推式f(i, j) = max(k * Vj + f(i - k * Wj, j - 1)) {k * wj <= i && k <= 第j件物品个数}

public class Solution {

    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {3, 2, 1}; //物品的质量
    int[] nums = {3, 2, 4}; //对应每件物品的数量

    public int maximumValue() {
        //背包大小为10
        int result = maximumValueHelper(10, 2);
        return result;
    }

    private int maximumValueHelper(int i, int j) {
        //base case:
        if (j < 0 || i == 0) {
            return 0;
        }

        int result = 0;

        //判断当前容量能否装进第j件物品
        if (i < ws[j]) {
            result = maximumValueHelper(i, j - 1);
        } else {
            //第j件物品可以取0~nums[j]个
            for (int k = 0; k <= nums[j] && k * ws[j] <= 10; k++) {
                int tmp = k * vs[j] + maximumValueHelper(i - k * ws[j], j - 1);
                result = tmp > result ? tmp : result;
            }
        }
        return result;
    }
}

带记忆数组的递归仿照“01背包”中的解答可以写出,并没有实质上的改变。

动态规划

public class Solution{
    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {3, 2, 1}; //物品的质量
    int[] nums = {3, 2, 4}; //对应物品的个数

    public int maximumValue() {
        //背包大小为10
        int[][] dp = new int[11][3];
        
        //填充第一行
        for (int j = 0; j < dp[0].length; j++) {
            dp[0][j] = 0;
        }

        //接下来的格子按照递推式来填
        for (int i = 1; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                //如果背包剩余容量小于第j件物品的质量
                if (i < ws[j]) {
                    dp[i][j] = dp[i][j - 1];
                } else {
                    //第j件物品可以取多个
                    for (int k = 0; k < nums[j] && k * ws[j] <= i; k++) {
                        dp[i][j] = Math.max(dp[i][j], k * vs[j] + dp[i - k * ws[j]][j - 1]);
                    }
                }
            }
        }

        return dp[10][2];
    }
}

完全背包

完全背包的特点是每件物品可以取无限次,所以相比于多重背包少了一个约束条件k<nums[j],其递推式为f(i, j) = max(k * Vj + f(i - k * Wj, j - 1)) {k * wj <= i},递归解法和动态规划解法相比于多重背包,并无实质上区别,这里给出动态规划解法

public class Solution{
    int[] vs = {3000, 2000, 1500}; //物品的价值
    int[] ws = {3, 2, 1}; //物品的质量

    public int maximumValue() {
        //背包大小为10
        int[][] dp = new int[11][3];
        
        //填充第一行
        for (int j = 0; j < dp[0].length; j++) {
            dp[0][j] = 0;
        }

        //接下来的格子按照递推式来填
        for (int i = 1; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                //如果背包剩余容量小于第j件物品的质量
                if (i < ws[j]) {
                    dp[i][j] = dp[i][j - 1];
                } else {
                    //第j件物品可以取多个
                    for (int k = 0; k * ws[j] <= i; k++) {
                        dp[i][j] = Math.max(dp[i][j], k * vs[j] + dp[i - k * ws[j]][j - 1]);
                    }
                }
            }
        }

        return dp[10][2];
    }
}

总结

完全背包”和“多重背包”是“01背包”的扩展,其本质区别是物品可以取多少个,只要理清楚了这个区别,这三种背包问题也就不是事了。