背包问题汇总
背包问题分类与结题模板
背包问题就是给定一个数组nums,按照一定方式从中选取元素,使其组成背包容量target的问题。
背包问题按数组元素选取规则可以分为:
- 0/1背包:每个元素最多选取一次;
- 完全背包:每个元素可以复选;
- 组合背包:元素选取需要考虑顺序。
背包问题按照求解目的可以分为:
- 最值问题:求选取元素个数或其价值的最大值或最小值;
- 存在问题:求是否存在满足该条件的元素组合;
- 组合问题:求所有满足该要求的不同元素组合。
背包问题的固定解法就是动态规划,解题模板是两层循环,分别遍历数组nums和背包容量target,然后写状态转移方程。根据求解目的可以确定状态转移方程。
状态转移方程的选择:
- 最值问题: dp[i] = max/min(dp[i], dp[i-num]+1)或dp[i] = max/min(dp[i], dp[i-num]+num);
- 存在问题(boolean):dp[i]=dp[i]||dp[i-num];
- 组合问题:dp[i]+=dp[i-num]。
leetcode背包问题总结:
0/1背包:
- 416.分割等和子集
- 474.一和零
- 494.目标和
- 879.盈利计划
- 1049.最后一块石头的重量2
完全背包:
- 279.完全平方数
- 322.零钱兑换
- 377.组合总和4
- 518.零钱兑换2
- 1449.数位成本和为目标值的最大数字
组合背包:
- 1155.郑骰子的N中方法
【力扣刷题】_279.完全平方数
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class leetcode0279 {
public int numSquares(int n) {
//dp[i]表示和为i的完全平方数的最小个数
int[] dp = new int[n + 1];
//赋初值
for (int i = 0; i <= n; i++) {
dp[i] = i;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i],dp[i - j * j] + 1);
}
}
return dp[n];
}
}
【力扣刷题】_322.零钱兑换
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class leetcode0322 {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
//使状态初始值都为一个明显不可能的值,最后看状态值是否改变,改变了即说明存在兑换方案。
for (int i = 1; i <= amount; i++) {
dp[i] = amount + 1;
}
int n = coins.length;
for (int i = 0; i <= amount; i++) {
for (int j = 0; j < n; j++) {
if (coins[j] > i) continue;
dp[i] = Math.min(dp[i],dp[i - coins[j]] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
【力扣刷题】_377.组合综合4
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class leetcode0377 {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
int n = nums.length;
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int j = 0; j < n; j++) {
if (nums[j] > i) continue;
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
}
【力扣刷题】_416.分割等和子集(非常重要)
本题的难点在于理解题意,要能看懂这是一道背包问题。
0-1背包问题一定要先从二维动态规划来理解,再简化写成一维动态规划。
以容量target为内循环时,一定要从后往前循环,因为每次计算一个状态dp时需要依据的是上一行的两个状态值,如果从前往后会导致状态dp[j - num[i]]不是上一行状态,而是本行状态。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class leetcode0416 {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for (int i = 0; i < n; i++) {
sum += nums[i];
}
int flag = sum % 2;
if (flag == 1) return false;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < n; i++) {
//此处必须为从后往前遍历,这样才能保证状态dp[j - nums[i]]为上一行的状态,而不是本行已经改变了状态值。
for (int j = target; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
}
【力扣刷题】_474.一和零(非常重要)
这是一个背包不要求装满,要求撞到最大值的0/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
27public class leetcode0474 {
public int findMaxForm(String[] strs, int m, int n) {
//先统计每个字符串中0和1的个数
int len = strs.length;
int[][] scope = new int[len][2];
for (int i = 0; i < len; i++) {
for (int j = 0; j < strs[i].length(); j++) {
if (strs[i].charAt(j) == '0') {
scope[i][0]++;
}else {
scope[i][1]++;
}
}
}
//状态dp[i][j]为0个数最多为i个,1个数最多为j个时最大子集的大小。
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i < len; i++) {
for (int j = m; j >= scope[i][0]; j--) {
for (int k = n; k >= scope[i][1]; k--) {
dp[j][k] = Math.max(dp[j][k],dp[j - scope[i][0]][k - scope[i][1]] + 1);
}
}
}
return dp[m][n];
}
}
【力扣刷题】_494.目标和
递归:bfs,递归的话时间复杂度较大,此处时间复杂度为O(2^n)。
1
2
3
4
5
6
7
8
9
10
11
12
13public class leetcode0494 {
public int findTargetSumWays(int[] nums, int target) {
return bfs(nums,0,target,0);
}
public int bfs(int[] nums,int index,int target,int sum) {
if (index == nums.length) {
if (target == sum) return 1;
else return 0;
}
return bfs(nums,index + 1,target,sum + nums[index]) + bfs(nums,index + 1,target,sum - nums[index]);
}
}迭代:动态规划,此处优化逻辑较为复杂,只提供二维动态规划解法。
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
34public class leetcode0494 {
public int findTargetSumWays(int[] nums, int target) {
int n = nums.length;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
}
if (target > sum || target < - sum) return 0;
//本题设计到减号,这样的话动态规划状态边界应设为最大值和最小值
int[][] dp = new int[n][sum * 2 + 1];
//初始化
if (nums[0] == 0) {
dp[0][sum] = 2;
}else {
dp[0][sum - nums[0]] = 1;
dp[0][sum + nums[0]] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j < sum * 2 + 1; j++) {
//计算结果j可能是j-nums[i]加上nums[i]得到的,也可能是j+nums[i]减去nums[i]得到的。
//先判断是否查出范围,然后将两种情况的种数相加即可。
int plus = (j - nums[i] >= 0) ? dp[i - 1][j - nums[i]] : 0;
int mimus = (j + nums[i] <= sum * 2) ? dp[i - 1][j + nums[i]] : 0;
dp[i][j] = plus + mimus;
}
}
return dp[n - 1][sum + target];
}
}
【力扣刷题】_518.零钱兑换2(非常重要)
本题的特殊之处在于,硬币可以复用,但是硬币使用顺序不同的情况看做同一种,所以在每种组合方法中最好保证每个硬币连续复用,之后就不再使用该硬币。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class leetcode0518 {
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount + 1];
//计算种数的背包问题,和为0的状态初始化种数为1。
dp[0] = 1;
for (int i = 0; i < n; i++) {
//这里这个顺序遍历非常厉害呀,顺序遍历就把每个元素复用多次的情况加进来了,但是相同元素只能连续使用多次。
for (int j = coins[i]; j < amount + 1; j++) {
dp[j] = dp[j] + dp[j - coins[i]];
}
}
return dp[amount];
}
}
【力扣刷题】_879.盈利计划(未解决)
反正我目前是连答案都看不懂
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class leetcode0879 {
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
int len = group.length;
//状态dp[i][j][k]表示在有前i+1个工作时,使用员工小于或等于j个、盈利大于或等于k时,的工作计划种数。
int[][] dp = new int[n + 1][minProfit + 1];
//背包问题的初始化都非常重要
for (int i = 0; i < n + 1; i++) {
dp[i][0] = 1;
}
//状态转移方程
for (int i = 0; i < len; i++) {
for (int j = n; j >= group[i]; j--) {
for (int k = minProfit; k >= 0; k--) {
//没看懂答案此处为何Math.max(0,k - profit[i])
dp[j][k] = dp[j][k] + dp[j - group[i]][Math.max(0,k - profit[i])];
}
}
}
return dp[n][minProfit];
}
}
【力扣刷题】_1049.最后一块石头的重量2
本题的难点在于想到并转化为背包问题。其实和分割等和子集有点类似,只不顾此处不一定是二等分,而是找到分成两份的最小差值。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class leetcode1049 {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += stones[i];
}
int target = sum / 2;
//状态dp[i]表示元素所能组成的不大于i的最大值
int[] dp = new int[target + 1];
for (int i = 0; i < n; i++) {
for (int j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j],dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[target] * 2;
}
}
【力扣刷题】_1155.掷骰子的N中方法
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class leetcode1155 {
public int numRollsToTarget(int d, int f, int target) {
//状态
int[] dp = new int[target + 1];
//初始化
dp[0] = 1;
//状态转移方程
for (int i = 0; i < d; i++) {
for (int j = target; j >= 0; j--) {
int sum = 0;
for (int k = 1; k <= f; k++) {
if (k > j) break;
sum = sum + dp[j - k];
}
dp[j] = sum;
}
}
return dp[target];
}
}