背包问题总结(1到10)

背包问题一直都是动态规划中的经典题目,下面通过这篇博文,系统的梳理一下解决背包问题的思路和要点;主要是好记性不如烂笔头嘛,怕自己忘得快,还能翻出来这篇文章来复习一下(注:本文的所有解决思路均为动态规划,不涉及贪心法等算法)

三种背包的概念区分

首先,背包问题的题型逃不出三种,那就是01背包问题、完全背包问题和多重背包问题,我们先来了解一下这三种题型的区别在哪里

01背包(ZeroOnePack)

有N件物品和一个容量为V的背包。每种物品均只有一件。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

完全背包(CompletePack)

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

多重背包(MultiplePack)

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

三种背包的具体题目

01背包例题

LintCode92

Backpack
描述
在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]

你不可以将物品进行切割。

样例
样例 1:
输入: [3,4,8,5], backpack size=10
输出: 9

样例 2:
输入: [2,3,5,7], backpack size=12
输出: 12

解法1

    public int backPack(int m, int[] A) {
        int len = A.length;
        int[][] f = new int[len + 1][m + 1];
        Arrays.fill(f[0], 0);
        for (int i = 1; i <= len; i++)
            for (int j = 0; j <= m; j++) {
                if (j >= A[i - 1])
                    f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - A[i - 1]] + A[i - 1]);
                else
                    f[i][j] = f[i - 1][j];
            }
        return f[len][m];
    }

关于动态规划的解题步骤和转移方程,这里不再赘述(非本文重点)
注意

  • (1)第二个for循环里面,写成
    for (int j = 1; j <= m; ++j)

    for (int j = m; j >= 1; --j)
    都是可以的。
    为什么写成for (int j = m; j >= 1; --j)也可以呢? 因为是两层循环,不管j是从大到小,还是从小到大,计算f[i][j]的时候,f[i - 1][j - A[i - 1]]肯定已经在i-1这上一轮循环里面计算过了。
    但不能写成
    for (int j = m; j >= A[i]; --j) {
    f[i][j] = max(f[i - 1][j], f[i - 1][j - A[i - 1]] + A[i - 1]);
    }
    也不能写成
    for (int j = A[i - 1]; j <= m; ++j) {
    f[i][j] = max(f[i - 1][j], f[i - 1][j - A[i - 1]] + A[i - 1]);
    }
    因为f[i][0 … A[i]]这段一直都是0。
  • (2) for i和for j两个循环位置可以互换。
  • (3) 第一个循环不能反过来,第二个循环可以反过来。
    上面的代码也可以写成
    public int backPack(int m, int[] A) {
        int len = A.length;
        int[][] f = new int[len + 1][m + 1];
        Arrays.fill(f[0], 0);
        for (int i = 1; i <= len; i++)
            for (int j = 0; j <= m; j++) {
                f[i][j] = f[i - 1][j];
                if (j >= A[i - 1])
                    f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - A[i - 1]] + A[i - 1]);
            }
        return f[len][m];
    }

空间优化版本

    public int backPack(int m, int[] A) {
        int len = A.length;
        int[] f = new int[m + 1];
        f[0] = 0;
        for (int i = 1; i <= len; i++)
            for (int j = m; j >= A[i - 1]; j--) {
                f[j] = Math.max(f[j], f[j - A[i - 1]] + A[i - 1]);
            }
        return f[m];
    }

注意

  • (1)第二个for循环不能写成for (int j = A[i]; j <= m; j++)。为什么呢?因为
    里面的f[j] = max(f[j], f[j - A[i]] + A[i])实际上相当于
    f[i][j] = max(f[i - 1][j], f[i - 1][j - A[i]] + A[i])
    如果j是从大到小的话,那么f[j]计算的时候f[j - A[i]]确实是上一轮的f[j - A[i]]。
    但是如果j是从小到大的话,那么f[j]计算的时候f[j - A[i]]就是本轮的f[j - A[i]]。那么结果就不对了。
    简单来说,就是我们采用一维数组进行优化时,j从大到小的话,就是数组从右向左进行计算,由于我们计算的时候只需要用到当前位置左边的数据,所以不会产生覆盖问题;但是如果j从小到大的话,就是数组从左向右进行计算,由于左边的数据会先被计算,所以当计算右边的数据时所用到的左边的数据已经被新值覆盖,所以结果是错误的。
    也就是说,对于j,我们要从大到小循环,才能保证f[j]计算的时候f[j - A[i]]确实是上一轮的f[j - A[i]]。
  • (2)上面的写法实际上等价于
    public int backPack(int m, int[] A) {
        int len = A.length;
        int[] f = new int[m + 1];
        f[0] = 0;
        for (int i = 1; i <= len; i++)
            for (int j = m; j >= 0; j--) {
                if (j >= A[i - 1])
                    f[j] = Math.max(f[j], f[j - A[i - 1]] + A[i - 1]);
                else
                    f[j] = f[j];
            }
        return f[m];
    }

这样来写就会更容易理解,因为空间优化后的代码并不是一蹴而就的,而是一步一步优化成最终的版本,所以我们进行优化的时候可以循序渐进,等到熟练了再直接尝试写出最终的优化代码。

LintCode125

Backpack II
有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.

问最多能装入背包的总价值是多大?

1.A[i], V[i], n, m 均为整数
2.你不能将物品进行切分
3.你所挑选的要装入背包的物品的总大小不能超过 m
4.每个物品只能取一次

样例
样例 1:

输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
样例 2:

输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
挑战
O(nm) 空间复杂度可以通过, 你能把空间复杂度优化为O(m)吗?
这个题目与LintCode92没什么区别,解题的一路也是一样的
解法1:
没有空间优化的经典DP解法:

    public int backPackII(int m, int[] A, int[] V) {
        int len = A.length;
        int[][] f = new int[len + 1][m + 1];
        Arrays.fill(f[0], 0);
        for (int i = 1; i <= len; i++)
            for (int j = 0; j <= m; j++) {
                if (j >= A[i - 1])
                    f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - A[i - 1]] + V[i - 1]);
                else
                    f[i][j] = f[i - 1][j];
            }
        return f[len][m];
    }

解法2:空间优化后的代码

    public int backPackII(int m, int[] A, int[] V) {
        int len = A.length;
        int[] f = new int[m + 1];
        f[0] = 0;
        for (int i = 1; i <= len; i++)
            for (int j = m; j >= A[i - 1]; j--) {
                f[j] = Math.max(f[j], f[j - A[i - 1]] + V[i - 1]);
            }
        return f[m];
    }

LintCode563

backPackV
描述
给出 n 个物品, 以及一个数组, nums[i] 代表第i个物品的大小, 保证大小均为正数, 正整数 target 表示背包的大小, 找到能填满背包的方案数。
每一个物品只能使用一次

样例
给出候选物品集合 [1,2,3,3,7] 以及 target 7

结果的集合为:
[7]
[1,3,3]
返回 2
思路:
这个题目是Backpack问题的一个变种,区别在于背包必须装满,返回值也不再是求最值,而是求装满的方案数。我们继续采用解决Backpack问题和Backpack II问题的思路,只需要将初始条件和转移方程稍作修改即可。
解法1:

    public int backPackV(int[] nums, int target) {
        int len = nums.length;
        int[][] f = new int[len + 1][target + 1];
        Arrays.fill(f[0], 0);
        f[0][0] = 1;
        for (int i = 1; i <= len; i++)
            for (int j = 0; j <= target; j++) {
                if (j - nums[i - 1] >= 0)
                    f[i][j] = f[i - 1][j] + f[i - 1][j - nums[i - 1]];
                else
                    f[i][j] = f[i - 1][j];
            }
        return f[len][target];
    }

解法2:空间优化

    public int backPackV4(int[] nums, int target) {
        int len = nums.length;
        int[] f = new int[target + 1];
        Arrays.fill(f, 0);
        f[0] = 1;
        for (int i = 1; i <= len; i++)
            for (int j = target; j >= nums[i - 1]; j--) {
                f[j] += f[j - nums[i - 1]];
            }
        return f[target];
    }

LintCode800

backPackIX
描述
你总共有n 万元,希望申请国外的大学,要申请的话需要交一定的申请费用,给出每个大学的申请费用以及你得到这个大学offer的成功概率,大学的数量是 m。如果经济条件允许,你可以申请多所大学。找到获得至少一份工作的最高可能性。

0<=n<=10000,0<=m<=10000

样例
样例 1:
输入:
n = 10
prices = [4,4,5]
probability = [0.1,0.2,0.3]
输出: 0.440

解释:
选择第2和第3个学校。

样例 2:
输入:
n = 10
prices = [4,5,6]
probability = [0.1,0.2,0.3]
输出: 0.370

解释:
选择第1和第3个学校。

思路:
这个题目也是01背包问题的变种,题目要求的是获得学校offer最大的概率,这样写转移方程的时候并不方便,所以我们稍微变通一下思路,用数组f[len][n]来保存在手头有n万元且len个学校可供选择的情况下,没有获得offer的最小概率。
解法一:

    public double backpackIX(int n, int[] prices, double[] probability) {
        int len = prices.length;
        double[][] f = new double[len + 1][n + 1];
        Arrays.fill(f[0], 1.0);
        for (int i = 1; i <= len; i++) {
            for (int j = prices[i - 1]; j <= n; j++) {
                f[i][j] = Math.min(f[i - 1][j], f[i - 1][j - prices[i - 1]] * (1.0 - probability[i - 1]));
            }
        }
        return 1 - f[len][n];
    }

解法二:空间优化版本

    public double backpackIX2(int n, int[] prices, double[] probability) {
        int len = prices.length;
        double[] f = new double[n + 1];
        Arrays.fill(f, 1.0);
        for (int i = 1; i <= len; i++)
            for (int j = n; j >= prices[i - 1]; j--) {
                f[j] = Math.min(f[j], f[j - prices[i - 1]] * (1.0 - probability[i - 1]));
            }
        return 1 - f[n];
    }

完全背包例题

LintCode440

backPackIII
描述
给定 n 种物品, 每种物品都有无限个. 第 i 个物品的体积为 A[i], 价值为 V[i].

再给定一个容量为 m 的背包. 问可以装入背包的最大价值是多少?

不能将一个物品分成小块.
放入背包的物品的总大小不能超过 m.
样例
样例 1:

输入: A = [2, 3, 5, 7], V = [1, 5, 2, 4], m = 10
输出: 15
解释: 装入三个物品 1 (A[1] = 3, V[1] = 5), 总价值 15.
样例 2:

输入: A = [1, 2, 3], V = [1, 2, 3], m = 5
输出: 5
解释: 策略不唯一. 比如, 装入五个物品 0 (A[0] = 1, V[0] = 1).
思路:
完全背包问题和01背包问题的区别就在于完全背包问题里面的物品都是不限次数使用的,所以我们要将for (int i = 1; i <= m; i++)放在外层循环,将for (int j = 0; j < len; j++)放在里层循环,这样就可以多次使用同一件物品。
代码:

    public int backPackIII(int[] A, int[] V, int m) {
        int len = A.length;
        int[] f = new int[m + 1];
        f[0] = 0;
        for (int i = 1; i <= m; i++)
            for (int j = 0; j < len; j++) {
                if (i >= A[j])
                    f[i] = Math.max(f[i], f[i - A[j]] + V[j]);
            }
        return f[m];
    }

LintCode564

backPackVI
描述
给出一个都是正整数的数组 nums,其中没有重复的数。从中找出所有的和为 target 的组合个数。

一个数可以在组合中出现多次。
数的顺序不同则会被认为是不同的组合。

样例
样例1

输入: nums = [1, 2, 4] 和 target = 4
输出: 6
解释:
可能的所有组合有:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
[4]
样例2

输入: nums = [1, 2] 和 target = 4
输出: 5
解释:
可能的所有组合有:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
思路:
这个题目属于Lint440的变种,所以仅仅调整初始状态和转移方程即可。
代码:

    public int backPackVI(int[] nums, int target) {
        int len = nums.length;
        int[] f = new int[target + 1];
        f[0] = 1;
        for (int i = 1; i <= target; i++) {
            for (int j = 0; j < len; j++) {
                if (i >= nums[j])
                    f[i] += f[i - nums[j]];
            }
        }
        return f[target];
    }

LintCode801

backPackX
描述
您总共有n元。 商家有三种商品,价格分别为150元,250元和350元。 这些商品的数量可以认为是无限的。 购买商品后需要将剩余的钱给商人作为小费,为商人找到最小的小费。
例子

范例1:
输入:n = 900
输出:0

范例2:
输入:n = 800
输出:0

思路:
这个题目也是比较标准的完全背包问题,思路与LintCode440那道题一样。
解法一:

    public int backPackX(int n) {
        int[] A = {150, 250, 350};
        int[] f = new int[n + 1];
        f[0] = 0;
        int len = A.length;
        for (int i = 1; i <= n; i++)
            for (int j = 0; j < len; j++) {
                if (i >= A[j])
                    f[i] = Math.max(f[i], f[i - A[j]] + A[j]);
            }
        return n - f[n];
    }

解法二:

    public int backPackX(int n) {
        int[] A = {150, 250, 350};
        int[] f = new int[n + 1];
        int len = A.length;
        for (int i = 1; i <= n; i++) {
            f[i] = i;
            for (int j = 0; j < len; j++) {
                if (i >= A[j]) {
                    f[i] = Math.min(f[i], f[i - A[j]]);
                }
            }
        }
        return f[n];
    }

注意:
解法二是用f[n+1]数组表示当前能剩的最少的小费,所以f[i] = i是用来对数组进行初始化的。

LintCode562

backPackIV
描述
给出 n 个物品, 以及一个数组, nums[i]代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target 表示背包的大小, 找到能填满背包的方案数。
每一个物品可以使用无数次

样例
样例1

输入: nums = [2,3,6,7] 和 target = 7
输出: 2
解释:
方案有:
[7]
[2, 2, 3]
样例2

输入: nums = [2,3,4,5] 和 target = 7
输出: 3
解释:
方案有:
[2, 5]
[3, 4]
[2, 2, 3]

思路:
LintCode562和LintCode564两个题目要区分开来,这两个题目虽然都属于完全背包问题,但是LintCode564可以使用之前两个题目的解法来解,而LintCode562不可以;举例说明,输入: nums = [2,3,6,7] 和 target = 7;方案[2,2,3]和方案[2,3,2]在LintCode564被看作是不同的方案,而在LintCode562中则被认为是一种方案,即LintCode562中的方法不区分具体的先后顺序。
代码:

    public int backPackIV(int[] nums, int target) {
        int len =nums.length;
        int[] f = new int[target + 1];
        f[0] = 1;
        for (int i = 0; i < len; i++)
            for (int j = nums[i]; j <= target; j++) {
                f[j] += f[j - nums[i]];
            }
        return f[target];
    }

注意:
我们来分析一下为什么这个代码会和LintCode564产生不一样的效果呢。我们拿样例1来举例说明:
输入: nums = [2,3,6,7] 和 target = 7
输出: 2
方案有:
[7]
[2, 2, 3]
为什么是[2,2,3]而不是[2,2,3]、[2,3,2]、[3,2,2]呢,我们来看LintCode562代码的两层for循环,for (int i = 0; i < len; i++)和for (int j = nums[i]; j <= target; j++),因为数组的循环是在外层,所以这个组合实际上是有顺序的,即[2,2,3]、[2,3,2]、[3,2,2]只出现了一个,就是[2,2,3]。因为外层循环首先定死的是i=0,所以nums[2]、nums[4]、nums[6]都在这次循环中得到了更新,当i=1时,[2,2,3]的组合就被计入了,而[2,3,2]、[3,2,2]永远不会出现,因为3总要出现在2后面。

多重背包例题

LintCode798

假设您有n元。 超市里有很多大米。 每种大米都装袋装,必须整袋购买。 给定每种大米的重量,价格和数量,找到可以购买的最大大米重量。
Backpack VII
例子

范例1:
输入:n = 8,价格= [3,2],重量= [300,160],数量= [1,6]
输出:640
说明:买第二米(价格= 2)用全部8元。

范例2:
输入:n = 8,价格= [2,4],重量= [100,100],数量= [4,2]
产量:400
说明:买第一个大米(价格= 2)用全部8元。
思路:
多重背包问题的思路很简单,把每种大米的数量amount当作是amount种大米,然后转化为每种只能只能使用一次的普通01背包问题进行解决即可。
解法一:

    public int backPackVII(int n, int[] prices, int[] weight, int[] amounts) {
        int len = prices.length;
        int[][] f = new int[len + 1][n + 1];
        for (int i = 1; i <= len; i++)
            for (int j = 0; j <= n; j++)
                for (int k = 0; k <= amounts[i - 1]; k++) {
                    if (j >= k * prices[i - 1]) {
                        f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - k * prices[i - 1]] + k * weight[i - 1]);
                    } else {
                        //相当于起到了初始化的作用
                        f[i][j] = Math.max(f[i][j], f[i - 1][j]);
                    }
                }
        return f[len][n];
    }

解法二:空间优化

    public int backPackVII(int n, int[] prices, int[] weight, int[] amounts) {
        int len = prices.length;
        int[] f = new int[n + 1];
        for (int i = 1; i <= len; i++)
            for (int j = n; j >= 0; j--)
                for (int k = 0; k <= amounts[i - 1]; k++) {
                    if (j >= k * prices[i - 1]) {
                        f[j] = Math.max(f[j], f[j - k * prices[i - 1]] + k * weight[i - 1]);
                    }
                }
        return f[n];
    }

LintCode799

Backpack VIII
说明
给一些不同价值和数量的硬币。
找出在1~n范围内这些硬币可以组合多少个值

例子

范例
n=10个
值=[1,2,4]
金额=[2,1,1]
返回:8
它们可以在1~8中组合所有值
思路:
首先这个题目当然可以像LintCode798那样的解法来解决,但是很遗憾,那样的思路在LintCode中超时了,代码如下
解法1:

    public int backPackVIII(int n, int[] value, int[] amount) {
        int len = value.length;
        boolean[][] f = new boolean[len + 1][n + 1];
        Arrays.fill(f[0], false);
        f[0][0] = true;
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= n; j++) {
                for (int k = 0; k <= amount[i - 1]; k++) {
                    f[i][j] = f[i][j] || f[i - 1][j];
                    if (j >= k * value[i - 1])
                        f[i][j] = f[i][j] || f[i - 1][j - k * value[i - 1]];
                }
            }
        }
        int count = 0;
        for (int i = 1; i <= n; i++)
            if (f[len][i])
                count++;
        return count;
    }

思路:
所以我们需要优化解法1的时间复杂度,具体的题解,可以参考这篇博文
解法二:

    //j层的循环为什么不能从右向左呢?
    //因为此题只需要判断是否存在即可,即f[i-1][j]为true时,f[i][j]一定为true,所以不需要用到上一行的数据
    //所以转移方程用到的是同一行的数据,同一行的数据要从左向右计算,因为计算右边的时候需要用到左边的数据
    //最里层的for循环也可以省略掉了,用if语句和数组来完成相同的功能
    public int backPackVIII(int n, int[] value, int[] amount) {
        int len = value.length;
        boolean[] f = new boolean[n + 1];
        f[0] = true;
        int sum = 0;
        for (int i = 1; i <= len; i++) {
            //count[n]用来存储当前第i-1个硬币为了凑价值j用了几个
            int[] count = new int[n + 1];
            for (int j = n; j >= value[i - 1]; j--) {
                if (!f[j] && f[j - value[i - 1]] && count[j - value[i - 1]] < amount[i - 1]) {
                    f[j] = true;
                    count[j] = count[j - value[i - 1]] + 1;
                    sum++;
                }
            }
        }
        return sum;
    }

参考博文

https://blog.csdn.net/qian2213762498/article/details/79857508
https://blog.csdn.net/na_beginning/article/details/62884939
https://blog.csdn.net/roufoo/article/details/83306065

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值