【动态规划七】背包问题

目录

0/1背包问题

一、【模板】01背包

二、分割等和子集

三、目标和

四、最后一块石头的重量 II

完全背包问题

一、【模板】完全背包

二、零钱兑换

三、零钱兑换 II

四、完全平方数

二维费用的背包问题

一、一和零

二、盈利计划

似包非包

组合总和

卡特兰数

不同的二叉搜索树


背包问题:

有1个背包,地上一堆物品,挑选一些物品放入背包中

问:能挑选物品的最大的价值是多少?

两个限制:

1.物品的属性(比如物品的体积,重量,  价值等)

2.背包的属性(背包的最大容量,承重等)

当物品的个数都限制为1个时,此时的背包问题就是0/1背包问题,因为每个物品选就是1个,不选就是0个

当物品的个数没有任何限制,都有无穷多个时,此时的背包问题就是完全背包问题

本篇博客介绍0/1背包问题

0/1背包问题

一、【模板】01背包

【模板】01背包_牛客题霸_牛客网 (nowcoder.com)icon-default.png?t=N7T8https://www.nowcoder.com/practice/fd55637d3f24484e96dad9e992d3f62e?tpId=230&tqId=2032484&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析

两个问题:

1.能装的物品的最大价值是多少?(可以装满,也可以不装满)

2.要求恰好装满,此时物品的最大价值是多少?

2.算法分析

问题一:

1.状态表示

背包问题本质还是线性dp问题,所以状态表示依旧是 经验 + 题目要求

dp[i]: 从前i个物品中挑选,所有选法中,能挑选的最大价值(×)

因为考虑第i个物品要不要放入背包的时候,必须先知道当前背包的容量才行

dp[i][j]: 从前i个物品中挑选,总体积不超过j, 所有选法中,能挑选出来的最大价值

2.状态转移方程

3.初始化

物品的编号从1开始,因此给dp表添加一行一列

第一行表示从前0个物品中挑选, 体积不超过j,能挑选的最大价值, 全是0

第一列表示从前i个物品中挑选, 体积不超过0,能挑选的最大价值, 全是0

4.填表顺序

从上往下填表

5.返回值

dp[n][V]

问题二:

1.状态表示

dp[i][j]: 从前i个物品中挑选,总体积正好等于 j,  所有选法中,能挑选出来的最大价值

2.状态转移方程

3.初始化

物品的编号从1开始,因此给dp表添加一行一列

dp[0][0] 表示从前0个物品中挑选,体积恰好为0的最大价值,什么都不选即可,那就是0

第一行的其余位置:

从前0个物品中挑选,价值恰好为j(j > 0)的最大价值,由于0个物品,所以凑不出价值恰好为j,因此值都是-1

第一列的其余位置:

从前i个物品中挑选,体积恰好为 0 的最大价值,一个物品都不选即可, 所以都是0

4.填表顺序

从上往下填表

5.返回值

dp[n][V] == -1 ? 0 : dp[n][V]

3.算法代码

#include <iostream>
using namespace std;
#include <string.h>const int N = 1010;
int n, V, v[N], w[N]; //物品个数,背包体积,物品体积数组,物品价值数组
int dp[N][N];int main() 
{//读入数据cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];//第一问for(int i = 1; i <= n; i++){for(int j = 1; j <= V; j++){dp[i][j] = dp[i-1][j];if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);}}cout << dp[n][V] << endl;//第二问memset(dp, 0, sizeof(dp)); //清空dp表for(int j = 1; j <= V; j++) //初始化dp表dp[0][j] = -1;for(int i = 1; i <= n; i++){for(int j = 1; j <= V; j++){dp[i][j] = dp[i-1][j];if(j >= v[i] &&  dp[i-1][j-v[i]] != -1) dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);}}cout << (dp[n][V] == -1 ? 0 : dp[n][V])  << endl;
}

四、空间优化

我们在最开始讲解动态规划时就提到过,背包问题中经常可以使用空间优化,也就是不必使用二维数组,根据状态转移方程, 填写dp[i][j]时,只需要dp[i-1][j]与dp[i-1][j-v[i]]的值,因此只需要两行即可,也就是两个一维数组即可

而我们可以i进一步优化,也就是只需要一个一维数组即可, 也就是用dp[i-1][j-v[i]]的值与dp[i-1][j]的值在原始数组上更新dp[i][j]即可, 不过要注意,由于在原始数组上更新,防止发生覆盖,需要从右向左更新, 而代码只需要把所有的二位数组的第一维全删掉,填表的第二层for循环从右向左填并且可以把更新状态转移方程的条件放在循环条件的判断处即可

#include <iostream>
using namespace std;
#include <string.h>const int N = 1010;
int n, V, v[N], w[N]; //物品个数,背包体积,物品体积数组,物品价值数组
int dp[N];int main() 
{//读入数据cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];//第一问for(int i = 1; i <= n; i++)for(int j = V; j >= v[i]; j--)dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << dp[V] << endl;//第二问memset(dp, 0, sizeof(dp)); //清空dp表for(int j = 1; j <= V; j++) //初始化dp表dp[j] = -1;for(int i = 1; i <= n; i++)for(int j = V; j >= v[i]; j--)if(dp[j-v[i]] != -1) dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << (dp[V] == -1 ? 0 : dp[V])  << endl;
}

二、分割等和子集

416. 分割等和子集 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/partition-equal-subset-sum/1.题目解析

给定一个只包含正整数的数组,问是否可以将数组划分成元素和相等的两部分

2.算法分析

转化一下: 本质就是从数组中挑选一些数,看这些数之和能否等于sum/2即可

本题虽然不完全是0/1背包问题,但是可以划分到该0/1背包问题,因为每个数要么选,要么不选,相当于每个物品选或不选,而且本题比0/1背包问题更加简单,因为本题就是看数字能否凑成sum/2即可, 0/1背包问题求的是物品的价值和最大~

1.状态表示

dp[i][j]: 表示从前i个数中挑选,和能否凑成j

2.状态转移方程

3.初始化

添加有一行一列

dp[0][0] = true, 表示一个数都不选,是否可以凑成0, 当然可以

第一行的其余位置:  i =0, 表示一个数都不选,能否凑成1, 2, 3...., 当然不可以,值为false

第一列的其余位置:  j = 0, 表示从前i个数选,和能否凑成0, 一个都不选即可,值为true

4.填表顺序

从上往下填表

5.返回值

dp[n][sum/2]

3.算法代码

二维dp表:

class Solution {
public:bool canPartition(vector<int>& nums) {//求数组所有元素的和int sum = 0;for(auto x : nums)sum += x;if(sum % 2) return false; //和是奇数, 直接返回false//1.创建dp表int m = nums.size(), aim = sum / 2;vector<vector<bool>> dp(m+1, vector<bool>(aim+1));//2.初始化for(int i = 0; i <= m ;i++)dp[i][0] = true;//3.填表for(int i = 1; i <= m; i++){for(int j = 1; j <= aim; j++){dp[i][j] = dp[i-1][j];if(j >= nums[i-1]) //注意下标的映射关系dp[i][j] = dp[i][j] || dp[i-1][j-nums[i-1]];}}//4.返回值return dp[m][aim];}
};

滚动数组优化:

class Solution {
public:bool canPartition(vector<int>& nums) {//求数组所有元素的和int sum = 0;for(auto x : nums)sum += x;if(sum % 2) return false; //和是奇数, 直接返回false//1.创建dp表int m = nums.size(), aim = sum / 2;vector<bool> dp(aim+1);//2.初始化dp[0] = true;//3.填表for(int i = 1; i <= m; i++)for(int j = aim; j >= nums[i-1]; j--)dp[j] = dp[j] || dp[j-nums[i-1]];//4.返回值return dp[aim];}
};

三、目标和

494. 目标和 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/target-sum/1.题目解析

给定一个非负nums数组和整数target, 给每个数组元素前可以加正负号,使得最终表达式运算的结果 == target即可,求满足条件的表达式的数目

2.算法分析

题目本质就是将数组划分成一堆正数和负数,假如正数的和是a, 负数的和的绝对值是b, 那么a-b==target, 而我们知道a + b == sum, 消元得 a = (target + sum) / 2;

因此问题就转化成了在数组中挑一些数,使得这些数的和等于a, 问共有多少种选法?? 此时与题目二基本一样了, 仍然属于0/1背包问题

1.状态表示

dp[i][j]: 从前i个数中挑选,总和正好等于j, 一共有多少种选法

2.状态转移方程

3.初始化

添加一行一列

dp[0][0] = 1, 表示 从前 0个数中挑选,总和恰好为0, 只要一个数都不选即可

第一行的其余位置: 表示 从前 0个数中挑选,总和恰好为j(>0), 显然不可能,因此值都为0

第一列的其余位置: 表示 从前 i个数中挑选,总和恰好为0, 但是nums中的元素值可能为0,因此可能有很多选法,但实际上不用初始化,因为初始化是为了防止越界,而第一列j = 0, 如果要用dp[i-1][j-nums[i]]这个状态转移方程,必须满足j >= nums[i], 也就是nums[i]只能等于0,此时dp[i-1][j-nums[i]]就是 dp[i-1][0]了,也就是正上方的值,因此不会越界, 因此无需初始化

4.填表顺序

从上往下填表

5.返回值

dp[n][a]

3.算法代码

二维dp表:

class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int sum = 0;for(auto x : nums)sum += x;int a = (sum + target) / 2;if(a < 0 || (sum + target) % 2) return 0; //a小于0或者sum+target是奇数, 直接返回0//1.创建dp表int n = nums.size();vector<vector<int>> dp(n+1, vector<int>(a+1));//2.初始化dp[0][0] = 1;//3.填表for(int i = 1; i <= n; i++){for(int j = 0; j <= a; j++){dp[i][j] = dp[i-1][j];if(j >= nums[i-1])dp[i][j] += dp[i-1][j-nums[i-1]];}}//4.返回值return dp[n][a];}
};

滚动数组优化:

class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int sum = 0;for(auto x : nums)sum += x;int a = (sum + target) / 2;if(a < 0 || (sum + target) % 2) return 0; //a小于0或者sum+target是奇数, 直接返回0//1.创建dp表int n = nums.size();vector<int> dp(a+1);//2.初始化dp[0] = 1;//3.填表for(int i = 1; i <= n; i++)for(int j = a; j >= nums[i-1]; j--)dp[j] += dp[j-nums[i-1]];//4.返回值return dp[a];}
};

四、最后一块石头的重量 II

1049. 最后一块石头的重量 II - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/last-stone-weight-ii/description/1.题目解析

之前我们已经在<<优先级队列>>博客中讲解了"最后一块石头的重量 I", 每次选两块最重的石头,而本题每次随机选两个石头进行碰撞,如果最后还剩余了一块石头,那么求最终剩余石头的最小重量,如果没有剩余石头,那么返回0

2.算法分析

本题的难点仍然在于如何把原问题进行转化,假设给定的数组元素是[a, b, c, d, e]

每次随机选两个石头进行碰撞, 可以发现最终结果无非是给原始数组元素添加上正负号,使得最终

的结果最小即可, 而我们可以把数分成两堆,一堆是正数,一堆是负数,假设正数的和是a, 负数和的绝对值是b, 已知a + b == sum, 求a - b 的最小值(也可能是b-a), 而a - b要最小,也就是a/b要最接近 sum的一半,此时问题就变成了0/1背包问题

把每一个数看成一个物品,每个物品的体积和价值都是nums[i], 挑选一些物品,背包的容量为sum / 2, 问能够装的的最大价值

1.状态表示

dp[i][j]: 从前i个数选,总和不超过j, 此时的最大和

2.状态转移方程

3.初始化

添加一行一列

第一行表示从前0个物品中挑选, 体积不超过j,能挑选的最大价值, 全是0

第一列表示从前i个物品中挑选, 体积不超过0,能挑选的最大价值, 全是0

4.填表顺序

从上往下

5.返回值

dp[n][m]相当于 a, sum - dp[n][m]是b, 由于dp[n][m] <= sum / 2的, b - a 一定 >= 0,  因此返回 b - a 即 sum - 2 * dp[n][m]

3.算法代码
二维dp表:

class Solution {
public:int lastStoneWeightII(vector<int>& nums) {int n = nums.size();int sum = 0;for(auto x : nums)sum += x;int aim = sum / 2;//1.创建dp表vector<vector<int>> dp(n+1, vector<int>(aim+1));//2.填表for(int i = 1; i <= n; i++){for(int j = 1; j <= aim; j++){dp[i][j] = dp[i-1][j];if(j >= nums[i-1])dp[i][j] = max(dp[i][j], dp[i-1][j-nums[i-1]] + nums[i-1]);}}//3.返回值return sum - 2 * dp[n][aim];}
};

滚动数组优化:

class Solution {
public:int lastStoneWeightII(vector<int>& nums) {int n = nums.size();int sum = 0;for(auto x : nums)sum += x;int aim = sum / 2;//1.创建dp表vector<int> dp(aim+1);//2.填表for(int i = 1; i <= n; i++)for(int j = aim; j >= nums[i-1]; j--)dp[j] = max(dp[j], dp[j-nums[i-1]] + nums[i-1]);//3.返回值return sum - 2 * dp[aim];}
};

完全背包问题

一、【模板】完全背包

完全背包和0/1背包的区别就是完全背包问题中每个物品的个数是不限的

【模板】完全背包_牛客题霸_牛客网 (nowcoder.com)icon-default.png?t=N7T8https://www.nowcoder.com/practice/237ae40ea1e84d8980c1d5666d1c53bc?tpId=230&tqId=2032575&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D1961.题目解析

两个问题:

1.能装的物品的最大价值是多少?(可以装满,也可以不装满)

2.要求恰好装满,此时物品的最大价值是多少?

2.算法分析

问题一:

1.状态表示

dp[i][j]: 从前i个物品中挑选,总体积不超过j, 所有选法中,能挑选出来的最大价值

2.状态转移方程

3.初始化

添加一行一列

第一列不可能越界,因为第一列 j = 0, 而 dp[i][j-v[i]] + w[i] 是在 j >= v[i]的情况下成立的,因此v[i] 只能等于0, 因此不会越界。

第一行表示从前0个物品挑选,体积不超过 j的最大价值,全都初始化成0即可

4.填表顺序

从上往下填表,每一行从左往右

5.返回值

dp[n][V]

问题二:

1.状态表示

dp[i][j]: 从前i个物品中挑选,总体积正好等于j , 所有选法中,能挑选出来的最大价值

2.状态转移方程

3.初始化

添加一行一列

同理,第一列不可能越界

dp[0][0] 表示从前0个物品中挑选,体积恰好为0的最大价值,那就是0

第一行的其余位置: 从前0个物品中挑选,体积恰好为j(j>0)的最大价值,由于没有物品,所以体积不可能 > 0, 因此都是-1

4.填表顺序

从上往下填表,每一行从左往右

5.返回值

dp[n][V] == -1 ? 0 : dp[n][V]

3.算法代码

#include <iostream>
using namespace std;
#include <string.h>const int N = 1001;
int n, V, v[N], w[N];
int dp[N][N];int main() 
{cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];for(int i = 1; i <= n; i++){for(int j = 0; j <= V; j++){dp[i][j] = dp[i-1][j];if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i][j-v[i]] + w[i]);}}cout << dp[n][V] << endl;memset(dp, 0, sizeof(dp));for(int j = 1; j <= V; j++)dp[0][j] = -1;for(int i = 1; i <= n; i++){for(int j = 1; j <= V; j++){dp[i][j] = dp[i-1][j];if(j >= v[i] && dp[i][j-v[i]] != -1)dp[i][j] = max(dp[i][j], dp[i][j-v[i]] + w[i]);}}cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
}

4.空间优化

更新dp[i][j]会用到两个绿色格子的值,而空间优化的本质就是只用一个一维dp表

黄色表示还没更新的值,绿色表示已经更新过的值,因此要填dp[i][j], 必须要保证用到的两个格子已经变成了绿色格子才可以,因此必须从左往右填一维dp表!!  这也是和0/1背包空间优化不同的地方,0/1背包是从右往左填一维dp表的

#include <iostream>
using namespace std;
#include <string.h>const int N = 1001;
int n, V, v[N], w[N];
int dp[N];int main() 
{//输入cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];//问题一for(int i = 1; i <= n; i++)for(int j = v[i]; j <= V; j++)dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << dp[V] << endl;//问题二memset(dp, 0, sizeof(dp));for(int j = 1; j <= V; j++)dp[j] = -1;for(int i = 1; i <= n; i++)for(int j = v[i]; j <= V; j++)if(dp[j-v[i]] != -1)dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
}

补充:

在问题二的代码中,用状态转移方程之前要先判断要用到的状态是否存在,事实上,我们也可以不用判断,只要保证求完max之后不会对结果产生影响即可,因此在初始化的时候,可以初始化成-0x3f3f3f3f即可

#include <iostream>
using namespace std;
#include <string.h>const int N = 1001;
int n, V, v[N], w[N];
int dp[N];int main() 
{//输入cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];//问题一for(int i = 1; i <= n; i++)for(int j = v[i]; j <= V; j++)dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << dp[V] << endl;//问题二memset(dp, 0, sizeof(dp));for(int j = 1; j <= V; j++)dp[j] = -0x3f3f3f3f;for(int i = 1; i <= n; i++)for(int j = v[i]; j <= V; j++)dp[j] = max(dp[j], dp[j-v[i]] + w[i]);cout << (dp[V] < 0 ? 0 : dp[V]) << endl;
}

二、零钱兑换

322. 零钱兑换 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/coin-change/1.题目解析

给定整数数组coins和一个整数amount表示总金额,求凑成amount所需要的硬币最小个数

2.算法分析

1.状态表示

dp[i][j]: 从前 i 个硬币中挑选,总和正好等于 j, 所有选法中,最少的硬币个数

2.状态转移方程

3.初始化

dp[0][0] = 0

第一行其余位置: 0x3f3f3f3f (参考上一道题目的补充内容)

4.填表顺序

从上往下填表, 每一行从左往右

5.返回值

dp[n][amout] >= 0x3f3f3f3f ? -1 : dp[n][amount]

3.算法代码

二维dp表

class Solution {
public:int coinChange(vector<int>& coins, int amount){int n = coins.size();const int INF = 0x3f3f3f3f;vector<vector<int>> dp(n+1, vector<int>(amount+1));for(int j = 1; j <= amount; j++)dp[0][j] = INF;for(int i = 1; i <= n; i++){for(int j = 0; j <= amount; j++){dp[i][j] = dp[i-1][j];if(j >= coins[i-1])dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);}}return dp[n][amount] >= INF ? -1 : dp[n][amount]; }
};

空间优化:

class Solution {
public:int coinChange(vector<int>& coins, int amount){int n = coins.size();const int INF = 0x3f3f3f3f;vector<int> dp(amount+1, INF);dp[0] = 0;for(int i = 1; i <= n; i++)for(int j = coins[i-1]; j <= amount; j++)dp[j] = min(dp[j], dp[j-coins[i-1]] + 1);return dp[amount] >= INF ? -1 : dp[amount]; }
};

三、零钱兑换 II

518. 零钱兑换 II - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/coin-change-ii/description/1.题目解析

本题求的是凑成 amount 的硬币的组合个数

2.算法分析

1.状态表示

dp[i][j]: 从前 i 个硬币中挑选,总和正好等于 j, 一共有多少种选法

2.状态转移方程

3.初始化

dp[0][0] = 1, 表示从前 0 个硬币中挑选,总和恰好为0的,共有1种选法(就是什么都不选)

第一行的其余位置初始化成0即可, 表示从前 0 个硬币中挑选,总和恰好为 j (>0),共有0种选法

4.填表顺序

从上往下填表,每一行从左往右

5.返回值

dp[n][amount]

3.算法代码

二维dp表

class Solution
{
public:int change(int amount, vector<int>& coins) {int n = coins.size();vector<vector<int>> dp(n+1, vector<int>(amount+1));dp[0][0] = 1;for(int i = 1; i <= n; i++){for(int j = 0; j <= amount; j++){dp[i][j] = dp[i-1][j];if(j >= coins[i-1])dp[i][j] += dp[i][j-coins[i-1]];}}return dp[n][amount];}
};

空间优化

class Solution
{
public:int change(int amount, vector<int>& coins) {int n = coins.size();vector<int> dp(amount+1);dp[0] = 1;for(int i = 1; i <= n; i++)for(int j = coins[i-1]; j <= amount; j++)dp[j] += dp[j-coins[i-1]];return dp[amount];}
};

四、完全平方数

279. 完全平方数 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/perfect-squares/1.题目解析

给定一个数n,  返回和为n的完全平方数的最小数量

2.算法分析

转化成完全背包问题

1.状态表示

dp[i][j]: 从前 i 个完全平方数中挑选,总和正好等于 j, 所有选法中,最小的数量

2.状态转移方程

3.初始化

dp[0][0] = 0, 从前 0 个完全平方数中挑选,总和恰好等于 0,只需要0个完全平方数即可

第一行其余位置:

从前 0 个完全平方数中挑选,总和恰好等于 j( > 0),显然做不到,初始化成为0x3f3f3f3f即可

4.填表顺序

从上往下填表,每一行从左往右

5.返回值

dp[根号下n][n]

3.算法代码

二维dp表

class Solution 
{
public:int numSquares(int n) {//1.创建dp表int m = sqrt(n);vector<vector<int>> dp(m+1, vector<int>(n+1));const int INF = 0x3f3f3f3f;//2.初始化for(int j = 1; j <= n; j++)dp[0][j] = INF;//3.填表for(int i = 1; i <= m; i++){for(int j = 0; j <= n; j++){dp[i][j] = dp[i-1][j];if(j >= i*i)  dp[i][j] = min(dp[i][j], dp[i][j-i*i] + 1);}} //4.返回值return dp[m][n];}
};

 空间优化

class Solution 
{
public:int numSquares(int n) {//1.创建dp表int m = sqrt(n);vector<int> dp(n+1);const int INF = 0x3f3f3f3f;//2.初始化for(int j = 1; j <= n; j++)dp[j] = INF;//3.填表for(int i = 1; i <= m; i++)for(int j = i*i; j <= n; j++)dp[j] = min(dp[j], dp[j-i*i] + 1);//4.返回值return dp[n];}
};

二维费用的背包问题

一、一和零

474. 一和零 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/ones-and-zeroes/description/1.题目解析

给你一个二进制字符串数组 strs 和两个整数 m 和 n ,找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1

2.算法分析

本质就是在字符串数组中挑选一些字符串出来,需要满足两个条件(1和0的个数限制), 这类问题就是二维费用的背包问题(也分为二维费用的0/1背包问题 和 二维费用的完全背包问题

1.状态表示
dp[i][j][k] 表示 从前 i 个字符串中挑选,字符0的个数不超过j, 字符1的个数不超过k, 所有的选法中,最大的长度

2.状态转移方程

3.初始化

当i=0, 表示从前 0 个字符串中挑选,字符0的个数不超过j, 字符1的个数不超过k, 没有字符串可选,因此长度就是0, 全部初始化成0即可

4.填表顺序

i从小到大即可

5.返回值

dp[len][m][n]

len表示字符串的长度

3.算法代码

三维dp表

class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m+1, vector<int>(n+1)));for(int i = 1; i <= len; i++){//统计当前字符串中0/1字符的个数int a = 0, b = 0;for(auto ch : strs[i-1])if(ch == '0') a++;else b++;for(int j = 0; j <= m; j++){for(int k = 0; k <= n; k++){dp[i][j][k] = dp[i-1][j][k];if(j >= a && k >= b)dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-a][k-b] + 1); }}}return dp[len][m][n];}
};

空间优化

class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();vector<vector<int>> dp(m+1, vector<int>(n+1));for(int i = 1; i <= len; i++){//统计当前字符串中0/1字符的个数int a = 0, b = 0;for(auto ch : strs[i-1])if(ch == '0') a++;else b++;for(int j = m; j >= a; j--)for(int k = n; k >= b; k--)dp[j][k] = max(dp[j][k], dp[j-a][k-b] + 1); }return dp[m][n];}
};

二、盈利计划

879. 盈利计划 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/profitable-schemes/1.题目解析

n表示员工人数, minProfit表示至少需要产生的利润,profit数组和group数组对应, 第i种工作需要group[i]个员工完成,会产生profit[i]的利润,现在问有多少种选择工作的方法,使得能够产生的利润>=minProfit

2.算法分析

本质是二维费用的0/1背包问题

1.状态表示

dp[i][j][k]: 从前i个工作中挑选,总人数不超过 j, 总利润至少为 k, 一共有多少种选法

2.状态转移方程

3.初始化

dp[0][j][0],  i = 0表示没有计划, 没有计划就没有利润,因此 k 也是0,此时什么都不选就满足条件,因此将dp[0][j][0]初始化成1

4.填表顺序

i从小到大即可

5.返回值

dp[len][n][m]

len是group数组或profit数组的长度

3.算法代码

三维dp表

class Solution {
public:int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {const int MOD = 1e9 + 7;//1.创建dp表int len = g.size();vector<vector<vector<int>>> dp(len+1, vector<vector<int>>(n+1, vector<int>(m+1)));//2.初始化dp表for(int j = 0; j <= n; j++)dp[0][j][0] = 1;//3.填表for(int i = 1; i <= len; i++){for(int j = 0; j <= n; j++){for(int k = 0; k <= m; k++){dp[i][j][k] += dp[i-1][j][k];if(j >= g[i-1])dp[i][j][k] += dp[i-1][j-g[i-1]][max(0, k-p[i-1])];dp[i][j][k] %= MOD;}}}//4.返回值return dp[len][n][m];}
};

空间优化

class Solution {
public:int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {const int MOD = 1e9 + 7;//1.创建dp表int len = g.size();vector<vector<int>> dp(n+1, vector<int>(m+1));//2.初始化dp表for(int j = 0; j <= n; j++)dp[j][0] = 1;//3.填表for(int i = 1; i <= len; i++)for(int j = n; j >= g[i-1]; j--)for(int k = m; k >= 0; k--){dp[j][k] += dp[j-g[i-1]][max(0, k-p[i-1])];   dp[j][k] %= MOD;}//4.返回值return dp[n][m];}
};

似包非包

组合总和

377. 组合总和 Ⅳ - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/combination-sum-iv/description/1.题目解析

给定一个nums数组和target目标值,从nums数组中找出和为target的排列个数,每个数可以选择无数次,注意顺序不同算是不同的情况

2.算法分析

"背包问题"研究的本质是限制条件下的组合问题,也就是我们挑选物品没有考虑顺序,而本题是要考虑顺序的,因此本题不能用"完全背包"的方法来解决

1.状态表示

本题的状态表示方式是之前的博客没有提到的:

根据分析问题的过程中,发现重复子问题,抽象出一个状态表示

[a, b, c, d, e], 在整个区间中选择凑成总和为target, 如果第一个数选了a, 那只需要在整个区间凑成总和为target-a即可, 于是我们发现了重复子问题

dp[i]表示: 凑成总和为 i, 一共有多少种排列数

2.状态转移方程

3.初始化

dp[0] = 1,因为数组中元素>=1,因此要凑出总和为0, 什么都不选,也就是空集

4.填表顺序

从左往右

5.返回值

dp[target]

3.算法代码

class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<double> dp(target+1);dp[0] = 1;for(int i = 1; i <= target; i++)for(auto x : nums)if(i >= x) dp[i] += dp[i-x];return dp[target];}
};

卡特兰数

不同的二叉搜索树

96. 不同的二叉搜索树 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/unique-binary-search-trees/description/1.题目解析

给定整数n, 求由n个节点(值是1到n)组成的二叉搜索树有多少种

2.算法分析

1.状态表示 --- 根据分析问题的过程中,发现重复子问题,抽象出一个状态表示

随便选一个数,作为根,此时就只需要分析左子树和右子树有多少种二叉搜索树, 然后乘起来

dp[i]表示: 节点个数为i时,一共有多少种二叉搜索树

2.状态转移方程

节点为[1, 2, ... i], 随便选一个数j作为根,则左子树的节点个数为j-1, 右子树的节点个数为i-j,

dp[i] += dp[j-1] * dp[i-j]

(1<= j <= i)

3.初始化

dp[0] = 1,空树也是一颗二叉搜索树

4.填表顺序

从左往右

5.返回值

dp[n]

3.算法代码

class Solution {
public:int numTrees(int n) {vector<int> dp(n+1);dp[0] = 1;for(int i = 1; i <= n; i++)for(int j = 1; j <= i; j++)dp[i] += dp[j-1] * dp[i-j];return dp[n];}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/14078.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI大模型应用开发实践:4.基于 Chat Completions API 实现外部函数调用

基于 Chat Completions API 实现外部函数调用 2023年6月20日,OpenAI 官方在 Chat Completions API 原有的三种不同角色设定(System, Assistant, User)基础上,新增了 Function Calling 功能。 详见OpenAI Blog functions 是 Chat Completion API 中的可选参数,用于提供…

matlab使用教程(80)—修改图形对象的透明度

1.更改图像、填充或曲面的透明度 此示例说明如何修改图像、填充或曲面的透明度。 1.1坐标区框中所有对象的透明度 透明度值称为 alpha 值。使用 alpha 函数设置当前坐标区范围内所有图像、填充或曲面对象的透明度。指定一个介于 0&#xff08;完全透明&#xff09;和 1&#x…

15.回归问题

回归问题是机器学习领域中的核心问题之一&#xff0c;它旨在通过拟合数据点来建立数学模型&#xff0c;以预测因变量的值。回归问题不仅广泛应用于金融、医疗、工程等领域&#xff0c;也是数据分析和机器学习算法研究的重要基础。本文将深入探讨回归问题的基本概念、数学原理、…

软件设计师-上午题-计算题汇总

一、存储系统 - 存储容量计算&#xff08;字节编址、位编址、芯片个数&#xff09; 内存地址是16进制 内存地址编址的单位是Byte&#xff0c;1K1024B 1B 8 bit 1.计算存储单元个数 存储单元个数 末地址 - 首地址 1 eg. 按字节编址&#xff0c;地址从 A4000H 到 CBFFFH&…

ubuntu20.04 10分钟搭建无延迟大疆无人机多线程流媒体服务器

1.使用效果 无人机画面 2.服务器视频端口 3.使用教程 3.1.下载ubuntu对应软件包&#xff1a;系统要求ubuntu16以上 3.2修改端口&#xff08;config.xml文件&#xff09; 3.3启动服务 目录下输入&#xff1a;终端启动&#xff1a;./smart_rtmpd 后台启动&#xff1a;nohup ./…

安卓手机APP开发__平台的架构

安卓手机APP开发__平台的架构 目录 概述 安卓软件栈 Linux内核 硬件抽象层(HAL) 安卓运行时 原生的C/C代码库 Java API框架 系统APP 概述 安卓是一个开源的&#xff0c;基于Linux的软件栈&#xff0c;它创建一个设备和形式因素的很宽的矩阵。 下图展示了安卓平台的所有…

Kubernetes的灵魂核心:kube-scheduler

Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排系统&#xff0c;用于自动化容器化应用程序的部署、扩展和管理。在Kubernetes集群中&#xff0c;kube-scheduler是一个至关重要的组件&#xff0c;它负责将Pod&#xff08;Kubernetes中的最小部署单元&#xff0…

2024年电工杯高校数学建模竞赛(A题) 建模解析| 园区微电网风光储协调优化配置 |小鹿学长带队指引全代码文章与思路

我是鹿鹿学长&#xff0c;就读于上海交通大学&#xff0c;截至目前已经帮200人完成了建模与思路的构建的处理了&#xff5e; 本篇文章是鹿鹿学长经过深度思考&#xff0c;独辟蹊径&#xff0c;实现综合建模。独创复杂系统视角&#xff0c;帮助你解决电工杯的难关呀。 完整内容可…

Anti Desgin Vue 实现 表格可编辑、新增、删除功能

1、效果图 新增&#xff1a; 删除&#xff1a; 修改&#xff1a; 代码&#xff1a; <template><div><button click"add">添加</button><span style"margin-left: 8px"><template v-if"hasSelected">{…

C++语言基础光速入门笔记

目录 从C到CC和C语言的关系C编译器C面向对象程序设计标准库ANSI 标准C的使用场景标准化 安装 GNU 的 C/C 编译器g 应用说明g 常用命令选项 C 基本语法C 关键字三字符组 C 数据类型基本的内置类型typedef 声明枚举类型类型转换静态转换&#xff08;Static Cast&#xff09;动态转…

沃通国密根证书入根红莲花浏览器,共建国密HTTPS应用生态

近日&#xff0c;沃通CA与海泰方圆红莲花安全浏览器进一步达成合作&#xff0c;沃通新增国密根证书入根红莲花安全浏览器。此次入根合作&#xff0c;标志着沃通国密数字证书产品兼容性再次得到提升&#xff0c;进一步夯实国密应用根基。 沃通CA入根红莲花浏览器&#xff0c;自动…

手机端如何访问本地vue+vite项目,实现实时调试?

一、应用场景 h5&#xff08;vuevite&#xff09;嵌入app后&#xff0c;出现某种问题时&#xff0c;需要每次发布坏境后&#xff0c;才能才看效果&#xff0c;这种来回很耗时间&#xff0c;本文章在于解决手机端直接访问本地启动应用项目&#xff0c;无需重复发布坏境 二、实…

四川易点慧电商抖音小店未来商业新蓝海

在数字经济的浪潮中&#xff0c;电商行业日新月异&#xff0c;不断涌现出新的商业模式和平台。四川易点慧电商抖音小店作为其中的佼佼者&#xff0c;以其独特的商业模式和广阔的市场前景&#xff0c;正成为越来越多创业者和商家的首选。本文将从多个角度探讨四川易点慧电商抖音…

docker 指定jdk11镜像执行jar

dockerfile :下载jdk11 并将上传的jar 放入jdk11容器/root&#xff0c;改名为app.jar vi dockerfile 。。。。内容见下图 # 构建jdk11镜像 docker build -t demo . # 也可以通过jdk11镜像&#xff08;前提有jdk11镜像&#xff09;外挂载目录方式运行jar docker run --name d…

惠普电脑怎么进入bios?图文教程助你轻松上手!

进入BIOS&#xff08;基本输入/输出系统&#xff09;是在电脑启动时进行硬件初始化和设置的重要步骤之一。对于惠普&#xff08;HP&#xff09;电脑用户来说&#xff0c;了解如何进入BIOS是解决一些硬件和系统问题的关键。本文将介绍惠普电脑怎么进入bios的三种方法&#xff0c…

java学习和项目总结

java&#xff1a; JDK/JRE/JVM三者的关系 JVM&#xff1a;JVM是java进行编译的虚拟机&#xff0c;是Java 能够跨平台运行的核心 。 所有的java程序会首先被编译为.class的类文件&#xff0c;这种类文件可以在虚拟机上执行。也就是说class文件并不直接与机器的操作系统交互&a…

Day24:Leetcode:235. 二叉搜索树的最近公共祖先 + 701.二叉搜索树中的插入操作 + 450.删除二叉搜索树中的节点

LeetCode&#xff1a;235. 二叉搜索树的最近公共祖先 解决方案&#xff1a; 1.思路 对于当前节点x&#xff0c;如果x比p和q的值都大&#xff0c;说明&#xff0c;p和q在x的右子树里面&#xff0c;那么去x的右子树里面去寻找&#xff1b;对于当前节点x&#xff0c;如果x比p和…

Docker 开启 SSL 验证

最近看 OJ 项目的远程开发阶段&#xff0c;然后踩坑踩了 2 天&#x1f602; Docker 版本&#xff1a;在 CentOS 安装 sudo yum install docker-ce-20.10.9 docker-ce-cli-20.10.9 containerd.io Client: Docker Engine - CommunityVersion: 20.10.9API version: …

ESP-IDF使用Button组件实现按键检测的功能

ESP32使用Button组件实现按键检测的功能 ESP-IDF 组件管理LED 组件简介测试button组件写在最后 ESP-IDF 组件管理 IDF 组件管理器工具用于下载 ESP-IDF CMake 项目的依赖项&#xff0c;该下载在 CMake 运行期间自动完成。IDF 组件管理器可以从自动从组件注册表 或 Git 仓库获取…

vue.js基础组件4--下

1.动态组件 1.定义动态组件 利用动态组件可以动态切换页面中显示的组件。使用<component>标签可以定义动态组件&#xff0c;语法格式如下。 <component is"要渲染的组件"></component>上述语法格式中&#xff0c;<component>标签必须配合i…