引言

大家好啊,我是前端拿破轮😁。

跟着卡哥学算法有一段时间了,通过代码随想录的学习,受益匪浅,首先向卡哥致敬🫡。

但是在学习过程中我也发现了一些问题,很多当时理解了并且AC的题目过一段时间就又忘记了,或者不能完美的写出来。
根据费曼学习法,光有输入的知识掌握的是不够牢靠的,所以我决定按照代码随想录的顺序,输出自己的刷题总结和思考
同时,由于以前学习过程使用的是JavaScript,而在2025年的今天,TypeScript几乎成了必备项,所以本专题内容也将使用TypeScript,来巩固自己的TypeScript语言能力。

0-1背包理论基础

什么是0-1背包问题?

0-1背包问题是经典的动态规划问题之一。

问题描述

给定n个物品,每个物品有两个属性:

  • w[i]:第i个物品的重量(weight)
  • v[i]:第i个物品的价值(value)

再给一个背包容量W(能承受的最大总重量),问我们:

从这n个物品中选择若干个(每个物品最多只能选择一次),使得在不超过总容量W的前提下,获得的最大价值是多少?

这是纯正的*0-1背包问题。这里的“0-1”表示每个物品要么选(1),要么不选(0),不能部分选取或重复选取。

如何解决0-1背包问题

我们使用动规五部曲来解决:

  1. 确定dp数组及其含义

我们定义一个二维数组

1
dp[i][j]表示从0-i的物品中选择,背包容量为j的情况下,所能获取的最大价值
  1. 确定动态转移方程

所谓的动态转移方程,就是不同的dp[i][j]之间的关系,有点类似于高中数学学习的数列递归公式

那对于第i个物品(下标从0开始),我们有两种选择:

  • 不选第i个物品,那么最大的价值就和只在前i-1个物品中选择的最大价值是一样的,即dp[i][j] = dp[i - 1][j]
  • 选第i个物品(前提是背包能放下第i个物品,即j >= w[i]),那么最大价值就应该是,在第i个物品的价值v[i]加上前i-1个物品在背包容量为j-w[i]的情况下的最大价值dp[i - 1][j - w[i]]。我们要的是最大价值,所以dp[i][j]要取两者之间的最大值。
1
2
3
4
5
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
  1. 确定初始化条件

我们确定初始化条件一定要在确定状态转移方程之后,因为只有确定了状态转移方程,我们才能知道需要初始化哪些值。根据我们的状态转移方程dp[i][j]只与它正上方的值,和左上方的值有关。所以我们初始化的时候,只需要初始化最左边的一列和最上面的一行即可。

我们先来考虑第一列,即dp[i][0],回顾一下dp数组的含义,dp[i][0]表示0-i的物品中选取,装到背包容量为0的背包中,所能装的最大价值。背包容量已经为0了,肯定无法装任何东西,所以这一列初始化为0

然后我们来考虑第一行dp[0][j],再回顾dp数组的含义dp[0][j]表示的是只能装第0个物品,背包容量为j,在这种情况下的最大价值。不能发现,如果背包能装下第0个物品,那么最大价值就是第0个物品的价值v[0],如果背包装不下第0个物品,那么最大价值就是0.

至于其他位置的值,因为我们的状态转移方程表面,当前位置的值只取决于正上方左上方,和当前位置自身值无关,所以其他位置初始化成多少都可以。都会在我们遍历的过程中被覆盖掉。

  1. 确定遍历顺序

由于我们要依靠第一行和第一列的值来得出其他位置的值,所以我们从左上角向右下角遍历。即从dp[1][1]开始遍历求值,直到dp[n][W]

  1. 打印dp数组

我们可以在运行过程中打印出最后的dp数组,看其是否符合我们对dp数组的定义。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const zeroOneBag = (w: number[], v: number[], totalWeight: number) => {
// 剪枝
if (w.length === 0 || v.length === 0 || totalWeight === 0) {
return 0;
}
// 保存物品数量
const n = w.length;

// 创建二维数组
const dp = new Array(n).fill(0).map(() => new Array(totalWeight + 1).fill(0));

// 初始化dp数组,因为所有值都初始化为了0,所以第一列不需要单独初始化
for (let j = 0; j <= totalWeight; j++) {
if (j >= w[0]) {
dp[0][j] = v[0];
}
}

// 遍历dp数组
for (let i = 1; i < n; i++) {
for (let j = 1; j <= totalWeight; j++) {
if (j < w[i]) {
// 当前背包容量小于物品重量,无法装入
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入取最大值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
}
// 返回结果
return dp[n - 1][totalWeight];
};

console.log(zeroOneBag([1, 3, 4], [15, 20, 30], 4));
// 输出:30
// 选第3个物品(w=4, v=30)

console.log(zeroOneBag([1, 3, 4, 5], [15, 20, 30, 50], 7));
// 输出:65
// 选第1个和第4个物品(w=1+5=6, v=15+50=65)

console.log(zeroOneBag([5, 6, 7], [10, 20, 30], 3));
// 输出:0
// 没有物品能放入背包

console.log(zeroOneBag([], [], 10)); // 输出:0
console.log(zeroOneBag([1, 2, 3], [10, 20, 30], 0)); // 输出:0
console.log(zeroOneBag([2, 2, 3], [3, 4, 5], 5)); // 输出:9

用一维数组优化空间复杂度

几乎所有的背包问题都有一个通用的降低空间复杂度的优化手段,就是把dp数组降维。为什么可以对dp数组降维呢?我们观察状态转移方程:

1
2
3
4
5
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}

不难发现dp[i][j]只和它的上一行,也就是dp[i - 1][某个值]有关。所以我们完全没有必要把整个二维数组保存下来,而是可以只用一个一维数组,每次在这个数组上原地刷新即可。

那么我们重新用动规五部曲来分析一下。

  1. 确定dp数组及其含义。

dp[j]表示用0-i的元素,装背包容量为j的背包,所能装的最大价值。有的同学看到这里可能会疑惑了,你这根本没有i啊,怎么能是用0-i的元素呢?确实,在dp数组的定义上我们只有dp[j],没有i
但是理解上还是建议大家理解为0-i的元素,装背包容量为j的背包,所能装的最大价值。虽然我们没有在数组中定义i,但是在循环中是有i的,也就是说这种方式只能降低空间复杂度,无法降低时间复杂度。
所以当第一次循环时dp[j]就相当于二维数组中的dp[0][j]。后续的都是一次类推。

  1. 确定动态转移方程

如果能理解上面说的,看似dp[j]中没有出现i,但他实际表示的是装背包容量为j的背包,所能装的最大价值,只不过是在循环中动态变化的。那么这里的动态转移方程就很好理解了。

1
2
3
4
5
if (j < w[i]) {
dp[j] = dp[j]
} else {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i];)
}

我们来仔细看一下这个式子,有的同学可能感觉很奇怪,你这第一个条件语句中的内容不是啥也没干吗?把dp[j]赋值给dp[j],相当于没有任何操作啊。
从结果来看,确实没有任何变化,但是拿破轮为了方便同学们理解,才这样显式地写了出来。
因为这两个dp[j]的含义是不一样的。dp[j] = dp[j],等号左边的dp[j]表示新的当前行的dp[j],等号右边的dp[j]表示的是上一行的dp[j]。也就是说,这一句实际上相当于二维数组中的

1
2
3
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
}

只不过由于我们现在只有一维数组了,在原地更新,所以左边的dp[j]就相当于dp[i][j],而右边的就相当于dp[i - 1][j],所以如果同学们直接去看题解可能会很懵,因为它会直接写成下面这样:

1
2
3
if (j >= w[i]) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}

看起来就像没有处理j < w[i]的情况一样,实际上真实的过程应该就像我们上面写的那样才对。

j >= w[i]的情况下,语义仍然相同,等号左边表示新的当前行的dp[i][j],等号右边表示的都是上一行的dp[i - 1][j]

  1. 初始化dp数组。

根据我们的状态转移方程,现在dp[j]只依赖于它左边的值了,所以我们对dp[0]初始化为0,表示背包容量为0时,总价值一定是0.至于其他值初始化为什么,关键就是第一行遍历的时候会用到。首先价值肯定不能是负数。
然后我们看动态转移方程中,我们的dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i])求得是两者中的最大值,那我们只需要初始化为最小的非负数就一定不会造成干扰,所以其他位置我们必须初始化为0就不能像二维数组一样其他位置随便初始化了

  1. 确定遍历顺序

这是最容易错的地方!!!

在二维数组中,我们先遍历物品,再遍历背包,或者先遍历背包,再遍历物品都可以。但是在一维数组中,我们必须先遍历物品,因为我们数组只有一行,不能一列一列的遍历。而且在遍历的时候,我们必须要从右往左,也就是从后向前遍历。
这是为什么呢?回顾我们刚才谈到的,在一维数组的情况下,其实dp[j]在等号左边就表示当前行的值,在等号右边就表示上一行的值。我们要用上一行的值来更新当前行的值。
dp[j]依赖于它左边的值,如果我们从左向右遍历,先把左边的值更新为当前行的值,再求后续的dp[j],那么dp[j]就会用到已经更新过的值,而我们想用的是上一行的值,所以就会出现错误。

直观上表现就是我们把某个物品取了多次,不符合0-1背包只取一次的要求。

  1. 打印dp数组,在最后可以打印dp数组进行检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const zeroOneBagWithOneDimensionalArray = (w: number[], v: number[], totalWeight: number) => {
// 剪枝
if (w.length === 0 || v.length === 0 || totalWeight === 0) {
return 0;
}

// dp[j]表示背包容量为j时的最大价值
const dp = new Array(totalWeight + 1).fill(0);

// 遍历物品
for (let i = 0; i < w.length; i++) {
// 遍历背包容量,一定要从后向前遍历,因为dp[j]依赖于dp[j - w[i]]
for (let j = totalWeight; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}

return dp[totalWeight];
};

// 写几组测试用例

console.log(zeroOneBagWithOneDimensionalArray([1, 2, 3], [6, 10, 12], 5)); // 22
console.log(zeroOneBagWithOneDimensionalArray([2, 3, 4], [3, 4, 5], 5)); // 7
console.log(zeroOneBagWithOneDimensionalArray([1, 3, 4], [15, 20, 30], 4)); // 35

有的同学可能会注意到,我们在遍历背包容量的时候,用的是倒序遍历,而且条件是let j = totalWeight; j >= w[i]; j--,只遍历到了w[i],并没有再往小的遍历,直到0,这是为什么呢?
因为当j < w[i]后,我们就会进行之前说的那个dp[j] = dp[j]的状态,实际上没有任何操作,不用遍历。

leetcode的0-1背包类问题

在leetcode上,并没有纯粹的0-1背包问题,而是有基于它的变式。核心思想是一致的。我们通过二维数组的方式理解了原理后,后续的题目拿破轮都将使用一维数组的方式进行代码的书写。

leetcode416-分割等和子集问题

leetcode题目链接

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

有的同学乍一看这个题目感觉有点懵,这和0-1背包有什么关系呢?根本没有背包啊!其实这道题目只需要稍加转化,就可以变成一个0-1背包的变种问题。

题目让我们把一个数组分割成两个和相等的子集,其实不就是从数组中选出若干个元素,判断能否凑成数组总和的一半吗?

我们可以把这个问题理解为,有nums这么多物品,重量是nums[i],有一个背包,容量是sum / 2,其中sum是数组所有元素的总和。问题就是让我们判断能否恰好把背包装满。由于每个物品只能取一次,所以是0-1背包问题。

我们还是按照动规五部曲来进行分析:

  1. 确定dp数组及其含义。

dp[j]表示选取0-i的物品,能否恰好凑满容量为j的背包,是一个布尔值。

  1. 确定动态转移方程

对于第i个元素,有两种可能,一种是不选第i个元素就能凑成j,一种是选了第i个(前提是j >= nums[i]),然后凑成了j。这两种情况中任意一个能凑成,那么对于第i个元素来说就能凑成。对于不选第i个就能凑成的情况来说,就是说dp[j]在上一行就是true,对于选了第i个元素能凑成的情况来说,也就是说本来能凑成j - nums[i]dp[j - nums[i]]true,那么加上当前的nums[i]dp[j]就肯定是true了。

所以应该用逻辑或来连接这两种情况。

1
2
3
if (j >= nums[i]) {
dp[j] = dp[j] || dp[j - nums[i]];
}
  1. 确定初始化值

dp[0]表示凑成0,那就什么都不取就可以,所以应该是true,而对于其他值,由于我们的动态转移方程是用的||连接的,所以肯定不能初始化为true,否则后续所有的都是true。所以我们初始化为false

  1. 确定遍历顺序

由于是一维数组,所以还是需要倒序遍历。

  1. 打印dp数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function canPartition(nums: number[]): boolean {
// 先求总和和数组中的最大值
let sum = 0;
let maxNum = 0;

for (const num of nums) {
sum += num;
maxNum = num > maxNum ? num : maxNum;
}

// 如果sum是奇数,直接返回false
if (sum & 1) return false;

// 求sum的一半,即我们的目标值target
const target = sum >> 1;

// 如果数组中的最大值大于target,也直接返回false。因为无论如何分,都可能有两个相等的子数组
if (maxNum > target) return false;

// 定义dp数组,dp[j]表示能否凑成j的容量
const dp = new Array(nums.length).fill(false);

// 初始化
dp[0] = true;

// 遍历物品
for (let i = 0; i < nums.length; i++) {
// 反向遍历背包容量
for (let j = target; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}

// 提前剪枝,如果只用当前物品就能凑出target,后续就不用看了,整个数组肯定能凑出
if (dp[target]) return true;
}

// 返回目标
return dp[target];
};

除了我们上述说的核心算法外,还根据题目加入了具体的剪枝代码。大家自行理解即可,应该比较好懂。

leetcode1049-最后一块石头的重量

leetcode题目链接

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

这个题目乍一看好像又和背包没什么关系,但是通过我们的小小经验,不难看出,稍加转化即可。要想让最后返回的石头总量尽可能的小,只需要将整个石头分成尽可能重量相近的两堆,最后剩余石头的重量就是两堆石头重量差值的绝对值。

我们还是按照动规五部曲来进行分析。

  1. dp数组的含义

dp[j]表示在0-i中任选,每个最多选一次,放入容量为j的背包中,不超过容量j的前提下能放入的最大重量。

  1. 确定动态转移方程

还是老规矩,对于第i个,有两种选择,要当前石头(前提是背包容量要大于当前石头重量)和不要当前石头,这两种情况下的最大值,就是应该更新的值。

1
2
3
4
5
if (j < stones[i]) {
dp[j] = dp[j];
} else {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
  1. 初始化dp

dp[0]表示在背包容量为0的情况下的最大重量,当然是0.而其他的值,由于后需要用Math.max,所以为了不影响后续,所以也初始化为0.

  1. 遍历顺序

一维数组还是先遍历物品,再遍历背包,遍历背包时从后向前,逆向遍历。

  1. 打印dp数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function lastStoneWeightII(stones: number[]): number {
// 求出数组总和
const sum = stones.reduce((acc, cur) => acc + cur, 0);

// 求出目标值
const target = sum >> 1;

// 定义dp数组
const dp = new Array(target + 1).fill(0);

// 遍历dp数组
for (let i = 0; i < stones.length; i++) {
for (let j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}

// 最后的结果应该是 (sum - dp[target]) - dp[target]
return sum - 2 * dp[target];
};

leetcode494-目标和

leetcode题目链接

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

这个题目乍一看好像和背包又没什么关系,但是经过前面的套路,我们已经深谙其道。稍微转换一下,本题就是要将数组分成两堆,一堆加+,一堆加-两堆相加后要求值为target,问我们构造两堆的方式数量。

我们设正数的和为pos,负数的和为neg。根据题目信息,我们可以得到以下信息:

1
2
pos + neg = target
pos - neg = sum

其中sum是整个数组的元素和。由上面两个式子,相加可得2pos = target + sum,即pos = (target + sum) / 2

那么问题就变成了从nums中选出若干个数,凑出pos,问可能的情况有哪些。

还是用动规五部曲来分析:

  1. 确定dp数组含义

dp[j]表示从0-i任取,填满容量为j的背包可能的方法数

  1. 确定动态转移方程

对于第i个物品,还是有取和不取两种情况。当然要想取还得j >= nums[i]。如果不取,当前值不用更新,直接用上一行的。如果要取,就要dp[j - nums[i]]

应该把这两种情况都算进去,所以要相加。

1
2
3
4
5
if (j < nums[i]) {
dp[j] = dp[j];
} else {
dp[j] = dp[j] + dp[j - nums[i]];
}
  1. 初始化dp数组

dp[0]表示凑出容量j可能的方法数,那就是一种,就是什么都不取。

所以dp[0] = 1。那其他的值,由于我们需要在动态转移方程中相加,所以为了不干扰后续,应该初始化为0.

  1. 遍历顺序

先物品,再背包容量,背包容量还是倒序遍历。

  1. 打印dp数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function findTargetSumWays(nums: number[], target: number): number {
// 求所有元素的和
const sum = nums.reduce((acc, cur) => acc + cur, 0);

// 求正数目标
// 如果sum + target是小于0或者是奇数,直接剪枝返回0
if ((sum + target) < 0 || (sum + target) & 1) return 0;

// 计算目标
const pos = (sum + target) >> 1;

// dp数组
const dp = new Array(pos + 1).fill(0);

dp[0] = 1;

// 开始遍历
for (let i = 0; i < nums.length; i++) {
// 倒序遍历背包容量
for (let j = pos; j >= nums[i]; j--) {
dp[j] = dp[j] + dp[j - nums[i]]
}
}

// 返回数量
return dp[pos];
};

leetcode474-一和零

leetcode题目链接

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

这个题目乍一看又和背包没关系,但是转换一下还是可以看出来的。不过这道题目和之前的确实不太一样,之前的背包只有重量这一个维度的属性,本题的背包可以看做有两个维度,一个是0的数量,另一个是1的数量

那就是问我们从0-i任选,在不超过背包容量限制的前提下,所能容纳的最大物品数目。

我们还是按照动规五部曲来进行分析:

  1. 确定dp数组的含义

由于背包中有两个维度,所以我们需要定义一个二维数组。dp[j][k]表示在0的数量限制为i,1的数量限制为j的情况下,从数组中任选,所能容纳的最多物品数量。

  1. 确定动态转移方程

对于第i个物品,还是有两个选择,不选择第i个物品和选择第i个物品。在容量不够的情况下,只能不选第i个物品,不用更新,在容量够的情况下,应该要选和不选中的最大值。

1
2
3
4
5
if (i >= zero(nums[i]) && j >= one(nums[i])) {
dp[i][j] = Math.max(dp[i][j], dp[i - zero(nums[i])][j - one(nums[i])] + 1);
} else {
dp[i][j] = dp[i][j]
}

这里的zero()one()使我们定义的辅助函数,用于获取物品中0和1的个数

  1. 初始化dp数组

dp[0][0]表示在0的容量为0,1的容量为0的条件下所能容纳物品的最大数量,自然是0.

对于其他dp,由于我们在动态转移过程中用了Math.max(),所以为了不干扰后续判断,也初始化为0.

  1. 遍历顺序。

还是先遍历物品,再遍历背包容量,只不过背包容量有两个维度,所以需要一个二重循环,并且也要倒序遍历。可能有的同学看到这疑惑了,不是说一维数组才需要倒序遍历吗?这里是二维的为什么也需要呢?其实不是一维需要,二维不需要,而是如果我们降低了本来的维度,就都需要后续遍历。这样才能保证不会用到新值。

而这个题目本来应该是一个三维数组dp[i][j][k],只不过我们把第一个维度,即物品维度i给压缩了。所以还是要后续遍历。

  1. 打印dp数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 辅助函数,返回一个二进制字符串中0和1的数量组成的数组
* @param s 二进制字符串
* @returns [零的数量, 一的数量]
*/
const getZeroAndOneQuantity = (s: string): number[] => {
let zero = 0;
let one = 0;
for(const c of s) {
// 由于字符串只由1和0组成,所以可以直接三目判断
c === '0' ? zero++ : one++;
}
return [zero, one];
}

function findMaxForm(strs: string[], m: number, n: number): number {
// 定义dp数组并初始化
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));

// 开始遍历
for (let i = 0; i < strs.length; i++) {
// 获取字符串中0和1的数量
const [zero, one] = getZeroAndOneQuantity(strs[i]);
// 倒序遍历容量
for (let j = m; j >= zero; j--) {
for (let k = n; k >= one; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - zero][k - one] + 1);
}
}
}

// 返回结果
return dp[m][n];
};

总结

本文先分析了0-1背包的理论基础,分别用二维数组的方式和一维数组的方式给出了实现代码。在做动态规划类问题的时候,应该牢记动规五部曲来进行分析。

  1. 确定dp数组及其含义
  2. 确定状态转移方程
  3. 确定dp数组的初始化值
  4. 确定遍历顺序
  5. 打印dp数组检查

这五步是环环相扣的,后面的步骤取决于前面的结果,所以千万不可颠倒次序。

比较容易出错的是遍历顺序,只要发生了降维,就必须后后序遍历,这样才能确保用到上一行的旧值,而不是新值。

接着我们以leetcode中关于0-1背包的几个变种题目进行分析,进一步巩固了对0-1背包的掌握。

我们来梳理一下:

  1. 纯粹的0-1背包问题是:每个物品最多选一次,给定容量,问能装的最大价值
  2. leetcode416分割等和子集问题是:每个物品最多选一次,给定容量,问能否恰好装满
  3. leetcode1049最后一块石头的重量问题是:每个物品最多选一次,给定容量,在不超过的前提下,求能装的最大重量
  4. leetcode494目标和的问题是:每个物品最多选一次,给定容量,问恰好装满背包的所有可能选择数量
  5. leetcode474一和零问题是:每个物品最多选一次,给定容量,但是容量有两个维度,问最多能装的物品数量

只要每个物品最多只能选择一次,那么他就是0-1背包问题。上述几个题目分别考察了0-1背包的不同角度。核心思路是相似的,就是最后求的结果不同而已。

好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。

往期推荐✨✨✨

我是前端拿破轮,关注我,一起学习前端知识,我们下期见!