算法-背包问题
一 、问题导入
如果不去刷力扣上的算法题目,可能我这辈子都不知道什么是背包问题的算法吧。中午吃完饭悠哉悠哉地打开了力扣的网站,选择了一道上面标注为“中等”的算法题。思考了一会,发现这种算法题超出了自己的只是层面,不愿花费时间去耗题目,于是看了官方的题解。只见那解释寥寥几行,略显敷衍,只见其提及背包算法。作为学生,必要的素养就是不会就要搞懂它,于是我打开了通用学习网站——哔哩哔哩,点击了第一个背包算法的视频。
看完之后,再看着官方的题解代码,便好与理解了。建议学习收藏——【动态规划】背包问题
二、力扣题目
首先看一下力扣的题目吧:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
官方提供的输入输出例子
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
官方的题解
public static int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for (String s : strs) {
int[] count = countzeroesones(s);
for (int zeroes = m; zeroes >= count[0]; zeroes--)
for (int ones = n; ones >= count[1]; ones--)
dp[zeroes][ones] = Math.max(1 + dp[zeroes - count[0]][ones - count[1]], dp[zeroes][ones]);
}
return dp[m][n];
}
三、背包算法
概述:
背包算法的问题描述可以大概地表示为:有一个固定容量大小的背包,容量为n,面对一些不同的物品(容量和价值各不相同),它们有共同的属性——容量和价值,求这些物品怎样组合才能使得装入背包的总价值最大(不一定要把背包装满)?
首先我们能够想到的就是排列组合的问题,但是这个问题的关键就是不知道要装入多少个物品,如果这类的问题可以说装入三个物品最大的价值(即要求得的数),我们便可以将三个物品的所有排列组合列举出来,求得最大值。这便是背包问题的关键之处。
概念讲完了,对于力扣的题目描述,我们可以把它与背包问题中的抽象联系起来:
背包: m, n 是两个背包,用来存放0和1;
物品: 字符串数组的每个字符串就是一个物品,物品里面含有0和1的数量。
价值: 最大子集,即使可以容纳的最多字符串的数量。
算法逻辑:
- 遍历每个物品,获得他们的属性
- 判断背包能不能把物品装下:
- 如果装不下,那么前n个物品的最佳组合就是前n-1物品的最佳组合
- 如果装的下:
- 用背包减去当前物品的容量后的容量,查看前n-1个物品的最佳组合,加上当前物品的价值就是前n个物品的最佳组合。
- 装完之后计算的最佳组合价值小于不装的话,则不装当前物品。
- 最后就能得到n个物品的最大价值
来个例子
// 计算容量为knapsack的时候,从products装入背包最大的价值
public static int maxValue(int knapsack, Product[] products) {
// 获得产品的数量
int len = products.length;
// 初始化一个数组,行为容量,列为前n个产品的最大价值
int[][] dp = new int[knapsack + 1][len + 1];
for (int i = 0; i < products.length; i++) {
Product product = products[i];
int capacity = product.capacity;
int value = product.value;
// 容量满足当前产品的情况下的最大价值
for (int j = knapsack; j >= capacity; j--) {
// 判断装入产品和不装入产品的最大价值
dp[j][i + 1] = Math.max(value + dp[j - capacity][i], dp[j][i]);
}
}
return dp[knapsack][len];
}
}
// 物品
class Product {
// 需要的容量
public int capacity;
// 物品价值
public int value;
public Product(int capacity, int value) {
this.capacity = capacity;
this.value = value;
}
}
输入的数据:
Product[] products = {
new Product(2,2), new Product(1,1), new Product(3,3), new Product(5,6)};
maxValue(5, products);
对于背包算法,最能通俗地去理解的就是代码中dp矩阵的作用,上面的输入数据到maxValue执行完成之后,dp数组的排列是这样的:
产品遍历序号0 | 产品遍历序号1 | 产品遍历序号2 | 产品遍历序号3 | 产品遍历序号4 | |
---|---|---|---|---|---|
背包容量0 | 0 | 0 | 0 | 0 | 0 |
背包容量1 | 0 | 0 | 1 | 0 | 0 |
背包容量2 | 0 | 2 | 2 | 0 | 0 |
背包容量3 | 0 | 2 | 3 | 3 | 0 |
背包容量4 | 0 | 2 | 3 | 4 | 0 |
背包容量5 | 0 | 2 | 3 | 5 | 6 |
而最终出现的答案便在右下角的值中。
解释:
上面的产品序号为0的时候,没有对应我们的product,因为数组默认值是0,这样定义的好处就是免去了一些判断。当产品序号为1的时候,产品的容量为2, 价值为2, 它的对应满足它容量的背包容量的最大价值就是它本身,因为前面没有产品。
当产品序号为2的时候,它的容量为1,价值为1,毫无疑问,当背包容量大于2+1的时候,它的最佳价值就是前一个序号的最佳价值加上本身的价值。
由此,我们可以得出,当产品序号为4的时候,它的容量为5,价值为6,如果它装入背包的话,背包则没有剩余容量,最大价值就是它本身6, 如果它不装入的话,最大价值就是它前一个序号的价值5,因为6比5大,所以最终还是装入了背包
四、分析力扣题目
回到一开始我从力扣中遇到的问题,它其实是背包问题的一种变式。不同的地方在于它是分别要装入0和1,相当于是两个背包,而且它的“价值”(通常来说是我们要求的那个数)其实就是1(代表子集)。按照力扣给出来的输入输出示例,我们可以得到它的dp矩阵是这样子的:
N的容量0 | N的容量1 | N的容量2 | N的容量3 | |
---|---|---|---|---|
M容量0 | 0 | 1 | 1 | 1 |
M容量1 | 1 | 2 | 2 | 2 |
M容量2 | 1 | 2 | 3 | 3 |
M容量3 | 1 | 2 | 3 | 3 |
M容量4 | 1 | 2 | 3 | 3 |
M容量5 | 1 | 2 | 3 | 4 |
解释:
当程序读取的字符串是“10”的时候,矩阵的变化如下所示:
N的容量0 | N的容量1 | N的容量2 | N的容量3 | |
---|---|---|---|---|
M容量0 | 0 | 0 | 0 | 0 |
M容量1 | 0 | 1 | 1 | 1 |
M容量2 | 0 | 1 | 1 | 1 |
M容量3 | 0 | 1 | 1 | 1 |
M容量4 | 0 | 1 | 1 | 1 |
M容量5 | 0 | 1 | 1 | 1 |
接下来读取字符“0001”,矩阵的变化如下所示:
N的容量0 | N的容量1 | N的容量2 | N的容量3 | |
---|---|---|---|---|
M容量0 | 0 | 0 | 0 | 0 |
M容量1 | 0 | 1 | 1 | 1 |
M容量2 | 0 | 1 | 1 | 1 |
M容量3 | 0 | 1 | 1 | 1 |
M容量4 | 0 | 1 | 2 | 2 |
M容量5 | 0 | 1 | 2 | 2 |
也就是说,上面显示1的背包,如M的容量为2,N的容量为3的背包,只能装入”10“和”0001“这两个字符串中的一个。 以此类推便可以得到最后的矩阵。
细心可以发现,在第一个产品的例子中,其实不用用到二维数组,只是为了方便理解用二维数组来解释。通过上面力扣的矩阵演示,我们第一个例子完全可以使用一维数组实现。这里不过多的叙述。
关于要得出具体的产品是哪些的话,便要使用回溯法。另一篇博客再谈。