前言
背包模型的核心在于:n
个物品做一些选择,使得最终 值最大/值最小/数量最多等。解题思路就是:挨个物品考虑,每个物品有两种选择:选和不选。
背包最常见的两类模型:
- 01背包:
f(i, j) → f(i - 1, j), f(i - 1, j - v[i])
。f(i, j)
只依赖于 f(i - 1, j)
和 f(i - 1, j - v[i])
。因此base case是 f(0, j)
,一般 dp
数组定义为 dp[n + 1][w + 1]
,这样 dp[0][j]
的含义就是没有物品的时候的最优解,这样做是为了 便于初始化。 - 完全背包:
f(i, j) → f(i - 1, j), f(i, j - v[i])
。f(i, j)
只依赖于 f(i - 1, j)
和 f(i, j - v[i])
。可以看出,“完全背包”和“01背包”的区别只在于 选的时候 依赖不同,因为物品是不限制数量的,因此就算选了第 i 个物品,也可以继续考虑第 i 个物品。
代码模板如下:
01背包模板
1 2 3 4 5 6 7 8 9 10 11
| int n, C; int[] v, w; int[][] dp = new int[n + 1][C + 1]; dp[0][j]; for (int i = 1 ; i <= n ; i++) { for (int j = 0 ; j <= C ; j++) { if (j >= v[i - 1]) dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i-1]] + w[i - 1]); else dp[i][j] = dp[i - 1][j]; } } return dp[n][C];
|
完全背包模板
1 2 3 4 5 6 7 8 9 10 11
| int n, C; int[] v, w; int[][] dp = new int[n + 1][C + 1]; dp[0][j]; for (int i = 1 ; i <= n ; i++) { for (int j = 0 ; j <= C ; j++) { if (j >= v[i - 1]) dp[i][j] = max(dp[i-1][j], dp[i][j-v[i-1]] + w[i - 1]); else dp[i][j] = dp[i - 1][j]; } } return dp[n][C];
|
滚动数组:不难发现“01背包”和“完全背包”的依赖关系都取决于 (i - 1, j - a)
和 (i, j - a)
。因此可以用一个 一维数组 dp[j]
来表示这个过程,也就是初始化的时候 dp[j]
表示的是 i == 0
的值,然后依次将 dp[j]
改为 1, 2, 3, …., n的值。
01背包 的依赖如下:
对于任意节点(黄色)(i, j)
都依赖于上一行的两个值也就是 i - 1
这一行的值。因此可以 复用一个一维数组 来替代二维数组。注意:01背包使用一维数组表示一定要从一维数组从大到小来进行填表,否则会导致依赖的值被覆盖而导致出错!
01背包一维表示
1 2 3 4 5 6 7 8 9 10
| int n, C; int[] v, w; int[] dp = new int[C + 1]; dp[j]; for (int i = 1 ; i <= n ; i++) { for (int j = C ; j >= v[i - 1] ; j--) { dp[j] = max(dp[j], dp[j-v[i-1]] + w[i - 1]); } } return dp[C];
|
完全背包的依赖如下:
对于任意节点(黄色)(i, j)
都依赖于 (i - 1,j)
和 (i, j - v[i])
。因此依然可以 复用一个一维数组 来替代二维数组。注意:完全背包使用一维数组表示一定要从一维数组从小到大来进行填表,否则会出现依赖的值还没算而依赖错值!
1 2 3 4 5 6 7 8 9 10
| int n, C; int[] v, w; int[] dp = new int[C + 1]; dp[j]; for (int i = 1 ; i <= n ; i++) { for (int j = v[i - 1] ; j <= c ; j++) { dp[j] = max(dp[j], dp[j-v[i-1]] + w[i - 1]); } } return dp[C];
|
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
题目解析
“一个数组是否可以平均分为2份”等价于“能否找到一个子集,使得子集的和为数组总和的一半”。
因此,这就是一个01背包问题。数组中的数字就是物品,总和一半就是背包容量。
因此可以直接条用背包的模板。
- 状态转移方程式:
dp[i][j]
表示考虑前 i
个数字,能否凑出 j
。第 i
个数字可以选也可以不选,也就是 dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]
- 容器规模:
i
表示数字,范围为 [0, n]
。j
表示背包容量,范围为 [0, target]
- base case:
dp[0][j]
,含义是考虑第 0
个数字(也就是没有数字)的时候,能否凑出 j
,因此只能凑出0,所以 dp[0][0] = true
,其他的均为 false
。 - 填表顺序:从
i=1
行开始填,一行一行填即可。
代码展示
二维背包
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
| class Solution { public boolean canPartition(int[] nums) { int sum = 0; for (int num : nums) sum += num; if (sum % 2 != 0) return false; int n = nums.length; int target = sum / 2; boolean[][] dp = new boolean[n + 1][target + 1]; dp[0][0] = true; for (int i = 1 ; i <= n ; i++) { for (int j = 0 ; j <= target ; j++) { if (j >= nums[i - 1]) dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; else dp[i][j] = dp[i - 1][j]; } } return dp[n][target]; } }
|
一维背包
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
| class Solution { public boolean canPartition(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } if (sum % 2 != 0) { return false; } int n = nums.length; int target = sum / 2; boolean[] dp = new boolean[target + 1]; dp[0] = true; for (int i = 1; i <= n; i++) { for (int j = target; j >= nums[i - 1]; j--) { dp[j] = dp[j] || dp[j - nums[i - 1]]; } } return dp[target]; } }
|
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1]
,可以在 2
之前添加 '+'
,在 1
之前添加 '-'
,然后串联起来得到表达式 “+2-1”
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
题目描述
假设数组总和为 sum
,添加 -
号的元素的和为 neg
,则其余添加 +
号的元素之和为 sum - neg
,则我们要求的最终表达式是 +
号的元素减去 -
号的元素的值为 target
,也就是:
(sum−neg)−neg=sum−2⋅neg=target
将式子展开:
neg=(sum−target)÷2
因此,问题转换成 在数组中能找到多少个和为 neg
的子集。
这就是 01背包 问题。
- 状态转移方程式:
dp[i][j]
表示考虑前 i
个数字,能凑出多少个和为 j
的子集。第 i
个数字可以选也可以不选,也就是 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
- 容器规模:
i
表示数字,范围为 [0, n]
。j
表示背包容量,范围为 [0, neg]
- base case:
dp[0][j]
,含义是考虑第 0
个数字(也就是没有数字)的时候,能凑出多少个和为 j
的子集,因此只能凑出 0
,所以 dp[0][0] = 1
,其他的均为 0
。 - 填表顺序:从
i=1
行开始填,一行一行填即可。
代码展示
二维背包
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
| class Solution { public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int num : nums) { sum += num; } if ((sum - target) % 2 != 0) { return 0; } int neg = (sum - target) / 2; if (neg < 0) { return 0; } int[][] dp = new int[nums.length + 1][neg + 1]; dp[0][0] = 1; for (int i = 1; i <= nums.length; i++) { for (int j = 0; j <= neg; j++) { if (j >= nums[i - 1]) { dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]]; } else { dp[i][j] = dp[i - 1][j]; } } } return dp[nums.length][neg]; } }
|
一维背包
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
| class Solution { public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int num : nums) { sum += num; } if ((sum - target) % 2 != 0) { return 0; } int neg = (sum - target) / 2; if (neg < 0) { return 0; } int[] dp = new int[neg + 1]; dp[0] = 1; for (int i = 1; i <= nums.length; i++) { for (int j = neg; j >= nums[i - 1]; j--) { dp[j] = dp[j] + dp[j - nums[i - 1]]; } } return dp[neg]; } }
|
其他解法
这个 01背包 的转换可能并不是特别好想,也可以使用直观的 dp 进行表示,也就是加+号和加-号的情况。
dp[i][j] = dp[i -1][j + nums[i]] + dp[i - 1][j - nums[i]]
但是这种做法并不好做,因为 j 是会出现负数的情况的,因此我们需要进行处理,因为sum(nums)最大是1000,那全部加-号就是 -1000,因此我们可以整体加1000进行处理,将负数转成正数。
代码如下:
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
| class Solution { public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int num : nums) { sum += num; }
int[][] dp = new int[nums.length + 1][2010];
dp[0][0 + 1000] = 1;
for (int i = 1; i <= nums.length; i++) { for (int j = -sum; j <= sum; j++) { if (j - nums[i - 1] >= -sum) { dp[i][j + 1000] += dp[i - 1][j - nums[i - 1] + 1000]; } if (j + nums[i - 1] <= sum) { dp[i][j + 1000] += dp[i - 1][j + nums[i - 1] + 1000]; } } }
return dp[nums.length][target + 1000]; } }
|
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
示例 1:
输入: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 。
示例 2:
输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 ‘0’ 和 ‘1’ 组成
1 <= m, n <= 100
题目解析
此题是裸背包问题,不需要进行转换,不同的是背包的限制有两个维度,一个是 0 的数量,一个 1 的数量。
- 状态转移方程式:
dp[i][j][k]
表示考虑前 i
个字符串,有 j
个 0 和 k
个 1,可以选取的最大子集是多大。 dp[i][j][k] = max(dp[i-1][j][k] , dp[i-1][j - zeros[i]][k - ones[i]] + 1)
,zeros[i]
表示第 i
个字符串有多少个0,ones[i]
表示第 i
个字符串有多少个 1。 - 容器规模:
i
表示第几个字符串,范围是 [0, length]
。j
表示剩余的 0 的数量,范围是 [0, m]
。k
表示剩余 1 的数量,范围是 [0, n]
- base case:
dp[0][j][k]
考虑第 0 个字符(没有字符)的时候,可以凑出的最大子集,那就都是 0 就好了。 - 填表顺序:
i
从 1 开始填到 n
即可。
代码展示
三维背包
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 51 52 53 54 55 56
| class Solution { public int findMaxForm(String[] strs, int m, int n) { int len = strs.length; int[] ones = new int[len]; int[] zeros = new int[len]; for (int i = 0; i < len; i++) { ones[i] = count(strs[i], '1'); zeros[i] = count(strs[i], '0'); } int[][][] dp = new int[strs.length + 1][m + 1][n + 1];
for (int i = 1; i <= strs.length; i++) { for (int j = 0; j <= m; j++) { for (int k = 0; k <= n; k++) { if (j - zeros[i - 1] >= 0 && k - ones[i - 1] >= 0) { dp[i][j][k] = Math.max( dp[i - 1][j - zeros[i - 1]][k - ones[i - 1]] + 1, dp[i - 1][j][k] ); } else { dp[i][j][k] = dp[i - 1][j][k]; } } } } return dp[len][m][n]; }
public int count(String str, Character target) { int count = 0; for (char c : str.toCharArray()) { if (c == target) { count++; } } return count; } }
|
二维背包
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 51 52
| class Solution { public int findMaxForm(String[] strs, int m, int n) { int len = strs.length; int[] ones = new int[len + 1]; int[] zeros = new int[len + 1]; for (int i = 1; i <= len; i++) { ones[i] = count(strs[i - 1], '1'); zeros[i] = count(strs[i - 1], '0'); } int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= strs.length; i++) { for (int j = m; j >= zeros[i]; j--) { for (int k = n; k >= ones[i]; k--) { dp[j][k] = Math.max( dp[j - zeros[i]][k - ones[i]] + 1, dp[j][k] ); } } } return dp[m][n]; }
public int count(String str, Character target) { int count = 0; for (char c : str.toCharArray()) { if (c == target) { count++; } } return count; } }
|
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
题目解析
此题是典型的完全背包问题。
- 状态转移方程式:
dp[i][j]
考虑前 i
个硬币,凑出 j
元最少需要的硬币数。dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i])
- 容器规模:
i
的范围是 [0, coins.length]
,j
的范围是 [0, amount]
- base case:
dp[0][j]
表示考虑前 0
个硬币凑出 j
的最少硬币数,除了 dp[0][0] = 0
以外,其余的均凑不了,也就是没有硬币的情况下,只能凑出 0
,其他的金额都凑不了,因此这里用一个比较大的数字表示(因为 amount
最大是10^4,因此我们用10001表示最大值) - 顺序:
i = 1
开始填表。
代码展示
二维背包
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
| class Solution { public int coinChange(int[] coins, int amount) { int[][] dp = new int[coins.length + 1][amount + 1]; Arrays.fill(dp[0], 10001); dp[0][0] = 0; for (int i = 1; i <= coins.length; i++) { for (int j = 0; j <= amount; j++) { if (j >= coins[i - 1]) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1); } else { dp[i][j] = dp[i - 1][j]; } } } return dp[coins.length][amount] == 10001 ? -1 : dp[coins.length][amount]; } }
|
一维背包
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
| class Solution { public int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, 10001); dp[0] = 0; for (int i = 1; i <= coins.length; i++) { for (int j = coins[i - 1]; j <= amount; j++) { dp[j] = Math.min(dp[j], dp[j - coins[i - 1]] + 1); } } return dp[amount] == 10001 ? -1 : dp[amount]; } }
|
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins 中的所有值 互不相同
0 <= amount <= 5000
题目解析
此题是典型的完全背包问题。
- 状态转移方程式:
dp[i][j]
考虑前 i
个硬币,凑出 j
元有多少种方式。dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]
- 容器规模:
i
的范围是 [0, coins.length]
,j
的范围是 [0, amount]
- base case:
dp[0][j]
表示考虑前 0 个硬币凑出 j
的有多少种方式,除了 dp[0][0] = 1
以外,其余的均凑不了,也就是没有硬币的情况下,只能凑出 0,其他的金额都凑不了,因此 dp[0][j] = 0
, j∈[1, amount]
- 顺序:
i = 1
开始填表。
代码展示
二维背包
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
| class Solution { public int change(int amount, int[] coins) { int[][] dp = new int[coins.length + 1][amount + 1]; dp[0][0] = 1; for (int i = 1; i <= coins.length; i++) { for (int j = 0; j <= amount; j++) { if (j >= coins[i - 1]) { dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]; } else { dp[i][j] = dp[i - 1][j]; } } } return dp[coins.length][amount]; } }
|
一维背包
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
| class Solution { public int change(int amount, int[] coins) { int[] dp = new int[amount + 1]; dp[0] = 1; for (int i = 1; i <= coins.length; i++) { for (int j = coins[i - 1]; j <= amount; j++) { dp[j] = dp[j] + dp[j - coins[i - 1]]; } } return dp[amount]; } }
|